多线程
创建新线程
Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
Thread的实例方法start()启动线程
实现方式有以下几种:
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
上述代码使用的是继承Thread类来完成多线程的创建。
使用的类:Thread
使用方法:Thread的实例方法 start()
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
上述代码使用的是实现Runnable类来完成多线程的创建。
使用的类/接口:Thread Runnable
使用的方法:Thread的实例方法 start()
public class Main {
public static void main(String[] args) throws Exception {
// 实现实现Callable接口的类
MyCallable callable = new MyCallable();
/*
*FutureTask一个可取消的异步计算,FutureTask 实现了Future的基本方法,
*提空 start cancel 操作,可以查询计算是否已经完成,并且可以获取计算的结果。
*/
FutureTask<Integer> ft = new FutureTask<>(callable);
Thread thread = new Thread(ft);
thread.start();
Integer value = ft.get(); // 获取返回值
System.out.println(value); // 1
}
}
// 相比于Runnable,Callable有反回值,通过泛型来定义返回的类型
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1;
}
}
上述代码实现了Callable接口来完成多线程的创建
使用的接口/类:Callable,Thread,FutureTask
使用的方法:FutureTask的实例方法get(),Thread的实例方法start()
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new Runnable(){
public void run(){
System.out.println("start new thread!")
}
});
t.start(); // 启动新线程
}
}
上述代码使用了匿名类,第一个是Java8引入的lambda语法,第二个是普通的匿名类语法
使用的类/接口:Thread Runnable
使用的方法:Thread的实例方法 start()
线程执行与main方法执行的区别
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
/*
*打印的顺序
*main start...
*main end...
*thread run...
*thread end.
*/
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
try{ // 异常捕获
Thread.sleep(10) // 毫秒为单位
}catch(InterruptedException e){}
System.out.println("main end...");
}
}
/*
*打印的顺序
*main start...
*thread run...
*thread end.
*main end...
*/
Thread的静态方法setPriority()设置优先级
可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(10) // 1~10, 默认值5
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
Thread的静态方法yield()让出线程
public void run(){
Thread.yield();
}
静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
多线程的状态
在java中,一个线程只能启动一次。调用start()方法启动新线程并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
- New(初始化):新创建的线程,尚未执行;
- Runnable(运行):运行中的线程,正在执行
run()方法的Java代码; - Blocked(阻塞):运行中的线程,因为某些操作被阻塞而挂起;
- Waiting(等待):运行中的线程,因为某些操作在等待中;
- Timed Waiting(超时等待):运行中的线程,因为执行
sleep()方法正在计时等待; - Terminated(终止):线程已终止,因为
run()方法执行完毕。
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:
- 线程正常终止:
run()方法执行到return语句返回; - 线程意外终止:
run()方法因为未捕获的异常导致线程终止; - 对某个线程的
Thread实例调用stop()方法强制终止(强烈不推荐使用)。
join方法
public class Create {
public static void main(String[] args) throws InterruptedException {
System.out.println("main start");
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("thread start");
System.out.println("thread end");
}
};
thread.start();
thread.join(); // 可以设置失效时间(毫米)
System.out.println("main end");
}
}
/*
*打印的顺序
*main start...
*thread run...
*thread end.
*main end...
*/
当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印start,t线程再打印hello,main线程最后再打印end。
如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。(比如:用户下载一个100M的文件,网速太慢,用户点击取消,就是中断了线程)
使用Thread的实例方法interrupt()
public class Create {
public static void main(String[] args) throws InterruptedException {
System.out.println("main start");
Thread thread = new Thread(){
@Override
public void run() {
int count = 0;
while(true){
count++;
System.out.println(count);
}
}
};
thread.start();
Thread.sleep(1000);
thread.interrupt(); // 中断
System.out.println("main end");
}
}
述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。t线程要调用而t线程的while循环会检测isInterrupted(),isInterrupted()返回true会直接中断线程的执行
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
@Override
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!"); // 中断,抛出异常
}
hello.interrupt();
}
}
class HelloThread extends Thread {
@Override
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
如果线程处于等待状态,就是main线程调用了 t.join()等待t线程结束,如果之前调用 t.interrupt()中断t线程的话;t线程内部调用
hello.join()方法来等待hello线程结束时,会立刻抛出InterruptedException异常,捕获异常,就可以准备结束该线程了
在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。
设置标志位进行中断
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(10);
t.running = false;
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
@Override
public void run() {
int n = 0;
while (running) {
n++;
System.out.println(n + " hello!");
}
}
}
上述代码中,在HelloThread内部申明了running全局变量,由volatile修饰后,成为了线程共享的变量,线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
volatile的作用
这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。
因此,volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
守护线程
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。
如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
如上代码,调用Thread实例方法setDaemon(true),将此线程变成守护线程
线程同步
当线程各自运行时,无法手动干预线程的调度,因此,任何一个线程都有可能被系统干预暂停,然后在某个时间段在此运行
所以,当多个线程同时读写共享变量,会出现数据不一致的情况。
并发问题:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new AddRunnable());
Thread thread2 = new Thread(new DecRunnable());
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(Counter.count);
}
}
class Counter {
public static Integer count = 0;
}
class AddRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 100; i++){
Counter.count += 1;
}
}
}
class DecRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 100; i++){
Counter.count -= 1;
}
}
}
根据正常单线程模型下的运行结果:0,但是在多线程情况下,每次运行结果都是不一样的
如果要对多线程环境下的变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
假设 i=100 ,线程一与线程二要i++,这可能造成线程一与线程二执行完后最后的数是101
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IADD
│ │ISTORE (101)
│IADD │
│ISTORE (101)│
▼ ▼
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│-- lock -- │
│ILOAD (100) │
│IADD │
│ISTORE (101) │
│-- unlock -- │
│ │-- lock --
│ │ILOAD (101)
│ │IADD
│ │ISTORE (102)
│ │-- unlock --
▼ ▼
加锁和解锁就能保证三条指令能在一个线程间执行完成,不会有其他线程会进入此指令区间。
即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。
只有执行线程将锁释放后,其他线程才有机会获得锁并执行。
这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
使用synchronized关键字进行同步
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new AddRunnable());
Thread thread2 = new Thread(new DecRunnable());
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(Counter.count);
}
}
class Counter {
public final static Object lock = new Object();
public static Integer count = 0;
}
class AddRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 100; i++){
synchronized (Counter.lock){ // 获取锁
Counter.count += 1;
} // 释放锁
}
}
}
class DecRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 100; i++){
synchronized (Counter.lock){ // 获取锁
Counter.count -= 1;
} // 释放锁
}
}
}
synchronized保证了代码块在任意时刻最多只有一个线程能执行。
使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。
我们来概括一下如何使用synchronized:
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用
synchronized(lockObject) { ... }。
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
错误使用synchronized
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock1 = new Object();
public static final Object lock2 = new Object();
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock1) {
Counter.count += 1;
}
}
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock2) {
Counter.count -= 1;
}
}
}
}
上述代码结果不是0,虽然使用了synchronized。当时两个线程锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。
因此,使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[]{new AddThread(),new AddThread2(),new DecThread(),new DecThread2()};
for (Thread thread : threads) {
thread.start();
thread.join();
}
System.out.println(Counter.count1);
System.out.println(Counter.count2);
}
}
class Counter {
public final static Object lock = new Object(); // 同步锁
public static Integer count1 = 0;
public static Integer count2 = 0;
}
class AddThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++){
synchronized (Counter.lock){
Counter.count1++;
}
}
}
}
class DecThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++){
synchronized (Counter.lock){
Counter.count1--;
}
}
}
}
class AddThread2 extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++){
synchronized (Counter.lock){
Counter.count2++;
}
}
}
}
class DecThread2 extends Thread {
@Override
public void run() {
for(int i = 0; i < 100; i++){
synchronized (Counter.lock){
Counter.count2--;
}
}
}
}
上述四个线程对两个变量进行读写,但使用的是同一个Counter.lock对象锁,这就造成了原本可以并发执行的Counter.studentCount += 1和Counter.teacherCount += 1,现在无法并发执行了,执行效率大大降低。
实际上,需要同步的线程可以分成两组:AddThread和DecThread,AddThread2和DecThread2,组之间不存在竞争,因此,应该使用两个不同的锁
AddThread和DecThread使用Counter.lock
synchronized(Counter.lock) {
...
}
AddThread2和DecThread2使用Counter.lock2
synchronized(Counter.lock2) {
...
}
这样才能最大化地提高执行效率。
不需要synchronized的操作
JVM规范定义了几种原子操作:
- 基本类型(
long和double除外)赋值,例如:int n = m; (x64平台)jvm规定了64位也是原子性的 - 引用类型赋值,例如:
List<String> list = anotherList。
单条原子操作的语句不需要同步。
public class Main{
public Integer id;
public void set (Integer id){
this.id = id;
}
}
但是,如果是多行赋值语句,就必须保证是同步操作,
public class Main{
public Integer id;
public String name;
public void set (Integer id,String name){
this.id = id;
this.name = name;
}
}
有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。
public class Main{
public Object[] idAndName;
public void set (Integer id,String name){
Object[] idAndName = new Object[]{id,name};
this.idAndName = idAndName;
}
}
this.idAndName = idAndName是引用赋值的原子操作
idAndName是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。
为什么多行赋值是不安全的?
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
当执行完第一个赋值操作后,此时该线程被操作系统暂停。另外一个线程开始执行时,修改了第二个赋值操作的引用,然后第二个线程也被暂停,第一个线程继续执行后继续进行第二个变量的赋值操作。结果就会造成第二个线程中的变量被第一个线程中设置的替换了。
同步方法
让线程自己选择锁对象往往会使逻辑变的混乱,而且不利于封装。更好的方法是将synchronized封装起来。
public class Counter {
private Integer count = 0;
public void add(){
synchronized (this){
count++;
}
}
public void dec(){
synchronized (this){
count--;
}
}
public int get() {
return count;
}
}
这样一来,线程调用add()方法与dec()方法时它不必关心同步逻辑,因为synchronized代码块在add()、dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:
public class Main {
public static void main(String[] args) throws Exception {
Counter counter = new Counter();
Counter counter2 = new Counter();
// counter实例
new Thread(()->{
counter.add();
}).start();
new Thread(()->{
counter.dec();
}).start();
// counter2实例
new Thread(()->{
counter2.add();
}).start();
new Thread(()->{
counter2.dec();
}).start();
System.out.println(counter.get());
System.out.println(counter2.get());
}
}
如果一个类可以被多线程访问并且保证数据安全,那么这个类就是线程安全。Counter类就是线程安全的,类似Math这种只提供静态方法的也是线程安全的,除了线程安全的类,大多数类都是线程不安全的。如ArrayList,但是只是读取而不写入那么没有线程安全问题
上述的代码其实可以改进
public class Counter {
private Integer count = 0;
public synchronized void add(){ // 锁住this
count++;
} // 解锁
public synchronized void dec(){ // 锁住this
count--;
} // 解锁
public int get() {
return count;
}
}
因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
如果是静态方法加synchronized的话。锁住的是jvm自动创建的Class对象
get()方法因为是读取一个变量的,所以不需要同步,但如果是:
public class Counter {
private Integer id;
private Integer count;
public Num get(){
Num num = new Num();
num.id = this.id;
num.count = this.count;
return num;
}
}
就必须要同步了。
死锁
当一个对象拿到锁之后,可以调用其他需要这个锁的对象,就是锁可以重复使用(可重入锁)
public class Main {
public static void main(String[] args) throws Exception {
Counter counter = new Counter();
// counter实例
new Thread(()->{
counter.add();
}).start();
System.out.println(counter.get());
}
}
class Counter {
private Integer count = 0;
public void add(){
synchronized (this){
count++;
dec(); // 调用需要this锁的dec()方法
}
}
public void dec(){
synchronized (this){
count--;
}
}
public int get() {
return count;
}
}
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
死锁
public class Main {
public static void main(String[] args) {
MyClass myclass = new MyClass();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
myclass.add(10);
}
});
Thread thread2 = new Thread(() ->{
myclass.dec(10);
});
thread.start();
thread2.start();
}
}
class MyClass {
private final Object lockOne = new Object();
private final Object lockTwo = new Object();
private Integer count = 0;
public void add(Integer num){
synchronized (lockOne){ // 获取lockOne锁
try {
Thread.sleep(1000); // 睡眠,等待lockTwo锁被拿区
} catch (InterruptedException e) {
e.printStackTrace();
}
for (Integer i = 1; i <= num; i++) {
this.count+=i;
}
synchronized (lockTwo){ // 拿取lockTwo锁(通过等待,成功让lockTwo被其他线程获取)
dec(count);
}
}
}
public void dec(Integer num){
synchronized (lockTwo){ // 拿取lockTwo锁
for (Integer i = 1; i <= num; i++) {
this.count-=i;
}
synchronized (lockOne){ // 拿取lockOne锁(锁已经被获取)
add(count);
}
}
}
}
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()和dec()方法时:
- 线程1:进入
add(),获得lockAOne; - 线程2:进入
dec(),获得lockTwo。
随后:
- 线程1:准备获得
lockTwo,失败,等待中; - 线程2:准备获得
lockAOne,失败,等待中。
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。
因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
解决:线程获取锁的顺序要一致。