JUC 多线程并发编程
一、根本概念
1. 进程与线程
进程(Process):计算机中正在运转的程序的实例,是操作体系分配资源的根本单位。每个进程具有自己的内存空间、文件描绘符、数据栈等。
线程(Thread):进程中的一个履行单元。一个进程中至少有一个线程,一般称为主线程。线程是 CPU 调度和履行的最小单位。 线程同享进程的资源,一个进程中的多个线程能够并发履行,线程之间的通讯比进程之间的通讯更高效。
2. 并发与并行
并发(Concurrency):体系能够在同一时间段内处理多个使命,但这些使命或许并纷歧起履行。每个使命轮询持有cpu的时间片,在时间片内能够运用cpu,犹疑时间片很短,微观上看是并行履行。
并行(Parallelism):在同一时间有多个使命(线程、协程、事情驱动...)一起履行。一般需求多个 CPU 或多核处理器支撑,能够在物理上一起履行多个使命。
在实践中往往是并行和并发一起存在。
3. 同步与异步
同步(Synchronous):使命之间的履行是依照次序进行的,一个使命有必要等候另一个使命完结后才干开端履行。
异步(Asynchronous):使命之间的履行不需求等候,能够在使命履行的一起进行其他操作。使命之间能够独立进行,不需求等候。
留意:同步在多线程中是指 在多个线程拜访同享资源时,经过某种机制来确保同一时间只需一个线程能够拜访该资源,以避免数据纷歧致或竞赛条件
4. 预备作业
引进下面依靠,查看更多输出信息
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.11</version> <!-- 或许运用其他版别 -->
</dependency>
resources下创立一个 logback.xml
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 只输出时间和线程称号 -->
<pattern>%d{HH:mm:ss.SSS} [%thread] - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console" />
</root>
</configuration>
private static final Logger logger = LoggerFactory.getLogger(Main.class);
二、java的线程
1. 创立线程
-
承继 Thread 类并重写
run()
办法来界说线程的履行逻辑。然后,经过调用start()
办法来发动线程。Thread t1 = new Thread("threadName") { // 这儿咱们经过匿名类创立线程。(匿名类在创立时一起界说和实例化) @Override public void run() { System.out.println("hello world"); } }; t1.start(); // 发动 threadName 线程
-
完结 Runnable 接口,重写
run()
办法来界说线程要履行的代码。然后将该 Runnable 完结传递给一个 Thread 目标,调用start()
发动线程。class Work1 implements Runnable { @Override public void run() { System.out.println("hello world"); } } public class Main { public static void main(String[] args) { Runnable work1 = new Work1(); Thread t1 = new Thread(work1, "threadName"); // 创立线程 t1.start(); } }
下面是经过 Lambda 表达式优化创立进程
Runnable work1 = () -> System.out.println("hello world"); Thread t1 = new Thread(work1, "threadName"); t1.start(); // Thread t1 = new Thread(() -> System.out.println("hello world"), "threadName"); // t1.start();
-
FutureTask 是一个既能够作为使命履行器又能够获取使命成果。它完结了 Runnable 和 Future 接口,因而能够用来创立和办理线程使命,一起能够获取使命的履行成果。
// 创立使命 FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("hello world"); Thread.sleep(1000); return 1; } }); // 创立线程并且使命 Thread t1 = new Thread(task, "t1"); t1.start(); Integer returnValue = task.get(); // 会堵塞在这儿,知道获取到回来值 System.out.println(returnValue);
2. 常用办法
办法名 | 描绘 |
---|---|
start() |
发动线程,使线程进入 安排妥当状况,由 CPU 调度运转。 |
run() |
线程的履行逻辑,一般不直接调用,而是经过 start() 直接调用,调用 run() 不会敞开新线程。 |
sleep(long millis) (静态办法) |
使当时线程休眠指定的时间(毫秒),在此期间不会占用 CPU。(堵塞状况) |
join() |
等候线程履行完结后再持续履行后续代码。 |
interrupt() |
中止线程的履行,设置中止状况,但线程是否中止取决于代码逻辑(需手动检测或处理中止状况)。 |
isInterrupted() |
查看线程是否被中止(不中止状况时回来 false )。 |
interrupted() (静态办法) |
回来当时线程是否为中止状况,并把中止状况改为 false |
currentThread() (静态办法) |
获取当时履行的线程目标。 |
getName() / setName(String) |
获取或设置线程的称号,用于调试或标识线程。 |
getId() |
获取线程的仅有 ID。 |
setPriority(int) |
设置线程优先级(取值规模 1~10,默认值 5,优先级高的线程更简单被 CPU 调度)。 |
getPriority() |
获取线程优先级。 |
isAlive() |
查看线程是否还在运转状况。 |
yield() (静态办法) |
提示线程调度器让出 CPU,使当时线程由 运转状况 转为 安排妥当状况(或许会再次被当即调度)。 |
setDaemon(boolean) |
设置线程为看护线程(后台线程),当一切非看护线程完毕时,看护线程主动停止。有必要在线程发动前调用。 |
getState() |
回来线程的当时状况,成果为 Thread.State 枚举类型,或许的状况包括:NEW 、RUNNABLE 、BLOCKED 、WAITING 、TIMED_WAITING 和 TERMINATED 。 |
1. 根本办法的运用:
下面代码用不同办法创立了两个线程、设置了线程名 setName
,发动线程 start
,经过 Thread.sleep
让线程堵塞,经过 join
办法堵塞等候线程履行完毕
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
logger.debug("t1 start");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1");
Thread t2 = new Thread() {
@Override
public void run() {
try {
logger.debug("t2 start");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
t2.setName("t2"); // 设置线程名
t1.start();
t2.start();
logger.debug("主线程在作业");
t2.join(5000); // 最多等候5秒, 由于t2线程实践大约只需2秒,所以这个2秒就不堵塞了
logger.debug("t2 join end");
t1.join(); // 堵塞等候t1线程完毕
logger.debug("t1 join end");
}
}
// 输出, 调查输出能够看到,由于t2.join堵塞main线程,即便t1比t2先履行完,也无法输出
10:21:38.202 [t1] - t1 start
10:21:38.202 [t2] - t2 start
10:21:38.202 [main] - 主线程在作业
10:21:40.208 [main] - t2 join end
10:21:40.208 [main] - t1 join end
2. interrupt 的运用
interrupt 并不会让打断线程,而是让 isInterrupted 为 true,可是对处于在 sleep 或 wait 的线程(也便是等候的线程),并不会让 isInterrupted 为 true,而是抛出一个 InterruptedException
反常
调查下面代码能够看到 t1,t2的 isInterrupted 都是 false,t3的为 true。这儿看不懂t2的代码不要紧,下面会讲,只需理解上面那句话就能够。
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 创立线程 t1
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread curThread = Thread.currentThread();
logger.debug("t1 isInterrupted = {}", curThread.isInterrupted());
}
}, "t1");
// 创立线程 t2
Thread t2 = new Thread(() -> {
synchronized (obj) {
try {
obj.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread curThread = Thread.currentThread();
logger.debug("t2 isInterrupted = {}", curThread.isInterrupted());
}
}
}, "t2");
Thread t3 = new Thread(() -> {
while (true) {
boolean isInter = Thread.currentThread().isInterrupted();
if (isInter) {
logger.debug("t3 isInterrupted = true");
break;
}
}
}, "t3");
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
logger.debug("interrupt thread");
t1.interrupt(); // 打断t1
t2.interrupt(); // 打断t2
t3.interrupt(); // 打断t3
}
}
// 输出
11:04:51.551 [main] - interrupt thread
11:04:51.552 [t3] - t3 isInterrupted = true
11:04:51.552 [t2] - t2 isInterrupted = false
11:04:51.553 [t1] - t1 isInterrupted = false
3. 看护线程
一般线程发动后一定会履行完整个线程中的代码,只需存在一般线程没有履行完,整个项目就不会完毕。而看护线程却不是这样,看护线程纷歧定会履行完整个线程中的代码,当一切的一般线程都履行完后,即便看护线程还有未履行的代码,它也会被 JVM 强制停止。
看护线程一般用于后台的辅佐使命,例如废物收回(GC)线程、守时使命等。
- 一般线程
调查下面的输出能够看到,即便main线程的代码现已履行完了,可是会等候t1线程履行完,然后整个使用完毕public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } logger.info("t1 线程"); }, "t1"); t1.start(); Thread.sleep(1000); logger.info("履行完结"); } } // 输出 20:39:32.587 [main] - 履行完结 20:39:34.587 [t1] - t1 线程
- 看护线程
当main线程履行完后,看护线程t1 也完毕,没有输出 "t1 线程"。public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } logger.info("t1 线程"); }, "t1"); t1.setDaemon(true); // 设置t1为看护线程 t1.start(); Thread.sleep(1000); logger.info("履行完结"); } } // 输出 20:42:02.762 [main] - 履行完结
3. 线程的状况(java)
上面线程常用办法讲到 getState()
会回来java线程的6种状况,下面阐明下各个状况。
New
:线程目标已被创立,但没有调用了 start() 办法
Runnable
:线程已发动,并且能够被调度履行,处于可运转状况。线程处于 RUNNABLE 状况时,或许正在履行,也或许处于操作体系的安排妥当行列中等候 CPU 时间片分配。
Blocked
:当线程企图拜访某个目标的锁,而该锁现已被其他线程持有时,会从安排妥当行列移除,进入等候行列(堵塞行列),直到获取锁,堵塞状况下不会占用cpu,也不会分到时间片。但会进入等候行列时会有线程上下文切换会带来额定的 CPU 开支
Waiting
:线程因某些条件不满足而无期限挂起,直到它被唤醒。一般经过 Object.wait()
或 Thread.join()
进入此状况。等候状况下不会占用cpu,也不会分到时间片。
Timed Waiting
:线程进入此状况是为了等候某些条件产生,但与等候状况不同,超时等候是有时间约束的。一般经过 Thread.sleep(long millis)
、Object.wait(long timeout)
或 Thread.join(long millis)
等办法进入超时等候状况。不占用cpu
Terminated
:线程在履行完一切使命后进入 TERMINATED 状况,意味着线程已完结履行,或许因反常退出。
一些细节问题和证明:
-
java线程的
Runnable
状况包括 调用start办法后进入安排妥当行列后、占用cpu时、文件的读取时。尽管文件的读取在体系层面上看是堵塞的,可是java以为读取文件时依然是 Runnable 状况下面代码主线程会一向打印 t1 线程的状况,t1线程在 sleep 1秒后,读取文件。可是调查代码的输出能够看到
Timed Waiting
和Terminated
之间只需Runnable
状况,并没有Blocked
状况。能够证明读取文件的线程成语 Runnable 状况public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) throws InterruptedException, IOException { Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } Path path = Paths.get("test.txt"); try { byte[] bytes = Files.readAllBytes(path); } catch (IOException e) { throw new RuntimeException(e); } logger.info("t1 线程"); }, "t1"); t1.start(); Path path = Paths.get("out.txt"); while (true) { System.out.println(t1.getState()); Files.write(path, t1.getState().toString().getBytes(), StandardOpenOption.APPEND); } } } // 输出 TIMED_WAITING TIMED_WAITING RUNNABLE RUNNABLE RUNNABLE ...(满是RUNNABLE) RUNNABLE TERMINATED TERMINATED
三、多线程常见内容
临界区:在并发编程中,一段需求拜访同享资源的代码区域,并且这个代码区存在多个线程的写操作。
竞态条件:在并发程序中,多个线程并发拜访同享资源时,在没有正确同步操控的状况下并发地拜访同享资源,然后导致不行猜测的成果。
1. synchronized
调查下面代码,咱们在两个线程中一起拜访 静态变量 cnt,并对它进行写操作,导致输出成果并不是预期的0,是不确定的。
这是由于 cnt ++ 这个操作不具有原子性。cnt ++ 被jvm编译成字节码时有四步操作,分别是 getstatic (获取cnt),iconst_1 (预备常量1),iadd (履行 cnt + 1),putstatic (把cnt + 1赋值给cnt)。而这四个过程或许在履行期间被打断,比方时间片缺乏线程切换。
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
static int cnt = 0;
public static void main(String[] args) throws InterruptedException, IOException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
cnt++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
cnt--;
}
});
t1.start();t2.start();
t1.join();t2.join();
System.out.println(cnt);
}
}
// 输出
26026
1.1 synchronized 的概念
synchronized
关键字用于润饰代码块或办法。被润饰的代码块或办法需求传入一个目标锁,synchronized 能够确保同一时间只需一个线程能够获取到这个锁,其他没有获取到这个锁的线程会进入堵塞状况,只需获取到这个锁的才干够履行这个代码块,履行完代码块后会主动开释这个锁,确保了代码块的原子性。能够处理竞态条件的问题。
1.2 synchronized 润饰代码块
引进 synchronized 润饰代码块 cnt ++ 和 cnt --,传入的锁是一个静态变量 lock,线程t1和t2同享一个 lock, 所以t1和t2线程不会一起履行代码块中的内容。
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
static int cnt = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException, IOException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
synchronized (lock) { // 确保 cnt ++ 的原子性
cnt++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
synchronized (lock) { // 同一时间只会有一个线程获取到 lock 这个锁
cnt--;
}
}
});
t1.start();t2.start();
t1.join();t2.join();
System.out.println(cnt);
}
}
// 输出
0
1.3 synchronized 润饰办法
synchronized 用于办法上时,分为两种,实例办法和静态办法
实例办法
当 synchronized 用于实例办法时,它会 锁住调用该办法的实例目标。也便是说,同一个类的不同线程对同一个目标调用同步实例办法时,会排队等候履行,不能一起履行该办法。
当我多个线程都用 t1 这个实例调用 test 时,会排队等候履行。由于实例办法上加上synchronized 相当于锁住 当时实例 this
Test t1 = new Test();
t1.test();
class Test {
public synchronized void test() {
}
// 上面代码相当于下面注释这个,锁住的是 this, 这个示例中锁住的是 t1 这个实例
// public void test() {
// synchronized (this) {
// }
// }
}
静态办法
当 synchronized 用于静态办法时,它会 锁住类的 Class 目标(class目标对一切实例都同享,只需一份),即锁住整个类的类等级的资源,一切实例同享同一个锁。
当我多个线程都经过 Test.test()
调用时,会排队等候履行。
Test.test();
class Test {
public static synchronized void test() {
}
// 上面代码相当于下面注释这个,锁住的是Class目标
// public static void Test() {
// synchronized (Test.class) {
// }
// }
}
2. 线程安全性
在 Java 中,线程安全性一般是 多个线程 同享拜访同一资源时,不会导致数据过错或程序溃散。
1. 线程不安全的原因
线程不安全一般是由于以下原因引起的:
-
竞态条件(Race Condition):
竞态条件产生在多个线程一起拜访同享资源时,且至少有一个线程会修正资源。这时不同线程的履行次序或许会导致纷歧致的成果。 -
不行见性(Visibility):
一个线程对同享变量的修正或许在其他线程中不行见。由于线程或许会缓存局部变量或同享变量的副本,导致线程间的修正不能及时传达。 -
原子性(Atomicity):
原子性是指操作在履行进程中不能被中止。多个线程拜访同享资源时,假如没有恰当的同步机制,或许会导致非原子的操作,从而引发纷歧致的成果。
2. 引证类型局部变量线程安全性剖析
list 线程不安全,会呈现下标越界。由于有两个线程都在用 test 实例的 test1办法对同一个 list 目标进行修正,不同线程的履行次序导致了删去履行的次数比添加多,呈现越界。(竞态条件、原子性)
public class Main {
public static void main(String[] args) {
Test test = new Test();
new Thread(() -> test.test1()).start();
new Thread(() -> test.test1()).start();
}
}
class Test {
List<Integer> list = new ArrayList<>();
void test1() {
for (int i = 0; i < 10000; i++) {
test2();
test3();
}
System.out.println(list.size());
}
void test2() {
list.add(1);
}
void test3() {
list.remove(0);
}
}
list 线程安全。尽管同一个实例 test 的两个线程都在对 list 进行修正,可是 list 是局部变量,当调用test1时会创立新的list变量,两个线程修正的list不是同一个
public class Main {
public static void main(String[] args) {
Test test = new Test();
new Thread(() -> test.test1()).start();
new Thread(() -> test.test1()).start();
}
}
class Test {
void test1() {
List<Integer> list = new ArrayList<>(); // 不同点
for (int i = 0; i < 10000; i++) {
test2(list);
test3(list);
}
System.out.println(list.size());
}
void test2(List<Integer> list) {
list.add(1);
}
void test3(List<Integer> list) {
list.remove(0);
}
}
list 线程不安全。尽管 main 线程中创立的两个线程的 list 变量是两个不同的,可是test1中调用test3办法时把list传递了曩昔,test3创立了新的线程,这个新的线程同享了list,并且进行了修正。(竞态条件、原子性)
public class Main {
public static void main(String[] args) {
Test test = new Test();
new Thread(() -> test.test1()).start();
new Thread(() -> test.test1()).start();
}
}
class Test {
void test1() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
test2(list);
test3(list);
}
System.out.println(list.size());
}
void test2(List<Integer> list) {
list.add(1);
}
void test3(List<Integer> list) {
new Thread(() -> list.remove(0)).start();
}
}
3. 传统线程安全的类
传统线程安全的类一般包括 经过 synchronized
关键字来确保线程安全和经过不行变的性质,确保多个线程中不会呈现并发修正的状况