java多线程 在运行了这么久的单线程程序后,我们终于迈入了多线程的大门,在开始讲解之前,先来看一些基本的概念吧
基础知识 什么是线程?要回答这个问题,首先要知道什么是进程?简而言之,一个程序就是一个进程,每个进程独占一部分内存空间,但在古早时期,一个进程中只有一个线程或者说线程这个概念还没有诞生,此时的程序都是线性执行的,后来随着多核CPU的发展,为了提高CPU的利用率,人们想出了线程这一概念。
一个程序就是一个进程,独享一片内存空间,一个进程中包含多个线程,这些线程同时执行,共享同一片内存空间
如何实现多线程 想要实现一个简单的多线程应用,有以下三种方法
继承Thread
类
这是最为简单的方法,在要实现多线程的类中继承thread类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class People extends Thread { String name; public People (String name) { this .name = name; } void hello (String name) { System.out.println("Hello " +name); } @Override public void run () { hello(name); } }
在继承thread类后必须重写run方法,run方法将作为新线程的入口,此后在main方法中启动线程
1 2 3 4 5 6 public class Main { public static void main (String[] args) { People people=new People ("asd" ); people.start(); } }
这种方法简单高效,在实际使用时可以专门的写几个类作为多线程的启动器,分别在每个类中规定内个线程的行为,最后main方法只启动所有线程
实现Runnable
接口
此时的people
类改造为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class People implements Runnable { String name; public People (String name) { this .name = name; } void hello (String name) { System.out.println("线程运行中:" + Thread.currentThread().getName()); } @Override public void run () { hello(name); } }
main方法改造为
1 2 3 4 5 6 7 8 9 10 11 public class Main { public static void main (String[] args) { People people=new People ("asd" ); Thread thread1=new Thread (people); Thread thread2=new Thread (people); Thread thread3=new Thread (people); thread1.start(); thread2.start(); thread3.start(); } }
对于实现了runnable接口中的run方法的类,可以直接作为参数实现线程类同时开启新线程
当然,反正要传入一个对象,我为什么不能现在直接写呢
1 2 3 Thread thread4=new Thread (()->{ System.out.println(Thread.currentThread().getName()); });
直接用lambda表达式完成一个匿名内部类
使用 Callable
和 Future
这种方式的好处是能够获取线程运行的返回结果
1 2 3 4 5 6 7 8 9 10 11 12 public class People implements Callable <String> { String name; public People (String name) { this .name = name; } @Override public String call () throws Exception { return Thread.currentThread().getName(); } }
此时必须重写call函数作为线程入口
1 2 3 4 5 6 7 8 public class Main { public static void main (String[] args) throws ExecutionException, InterruptedException { People people=new People ("asd" ); FutureTask<String> futureTask=new FutureTask <>(people); new Thread (futureTask).start(); System.out.println(futureTask.get()); } }
其实方法远不止这几种,由于不断地开发,Java中实现相同功能的方法越来越多,很难说这是好事还是坏事,但暂时我就介绍这几种,剩下的如果将来由需要再做补充
接下来要面对的就是一些稍显复杂的东西了.看看这段代码,猜一猜它输出的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Main { private static int value = 0 ; public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程1完成" ); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程2完成" ); }); t1.start(); t2.start(); Thread.sleep(1000 ); System.out.println(value); } }
下面是运行结果
按照道理来讲,value的值应当是20000.而且显然这也不是某个线程未执行完毕的问题啊,根据打印顺序我们可以确定实际上是在所有的线程执行完成后才获取了value的值
事实上这是一种奇葩现象,并不是每次都出现,例如我将这一段程序执行了10次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Main { private static int value = 0 ; public static void main (String[] args) throws InterruptedException { for (int j=0 ;j<10 ;j++){ Thread t1 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程1完成" ); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程2完成" ); }); t1.start(); t2.start(); Thread.sleep(1000 ); System.out.println(value); value = 0 ; } } }
这是结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 线程2 完成 线程1 完成11495 线程2 完成 线程1 完成16662 线程2 完成 线程1 完成20000 线程1 完成 线程2 完成20000 线程2 完成 线程1 完成20000 线程1 完成 线程2 完成20000 线程1 完成 线程2 完成19138 线程2 完成 线程1 完成20000 线程2 完成 线程1 完成14428 线程1 完成 线程2 完成20000
你会发现有时结果正确有时不正确,不正确的结果数值还各不相同,这是为什么呢?
举个例子来解释,假设此时value的值为2,然后线程1与线程2在很短的时间内读取了该值,各自进行+1的操作,然后两个线程会先后的将3赋给value,此时两个线程各自执行了一个循环,但实际上value只加了1.这种巧合多次发生就造成了值的混乱.但是也会有某些时候恰好没有出现这种偶然,所以会的到一个正确的值
为了始终的到正确的值,我们需要对线程进行合理的调度,避免发生此类情况.这种调度的方法有很多,我们先看一看最简单的一种
线程的终止与休眠 首先是进程的休眠,即sleep()
方法,我们先来修改一下上面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Main { private static int value = 0 ; public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread (() -> { try { Thread.sleep(1000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程1完成" ); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程2完成" ); }); t1.start(); t2.start(); Thread.sleep(10000 ); System.out.println(value); } }
此时我们先让进程1休眠1秒确保线程2执行完毕再让线程一执行,确保最后获得正确的值(当然,这是一种很愚蠢的方法,后面我们会给出更正常的解决方法),此时通过适当的休眠我们无论运行多少次都能得到正确的值
接下来是关于线程的中断,注意,不是终止.线程的终止方法stop()
会强制停止该线程,这可能会造成资源未被释放等问题,而中断则是interupt()
方法,这个方法只会给相应的线程发出中断信号,然后由线程自行的进行下一步操作(也就是说线程收到中断信号后可以选择不中断),如果由必要,线程也可以将中断信号复位,接受下一次中断信号.我们再次对原来的程序进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Main { private static int value = 0 ; public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread (() -> { while (true ) { if (Thread.currentThread().isInterrupted()) { Thread.interrupted(); break ; } } for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程1完成" ); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; t1.interrupt(); System.out.println("线程2完成" ); }); t1.start(); t2.start(); Thread.sleep(1000 ); System.out.println(value); } }
这种方式似乎比上面的直接睡眠高明了不少,我们只需等待线程2执行的时间,而不是一个随意估计的数字,但这仍然不够,我们还有更高效的版本
线程的优先级 我们都能想到一个事实,CPU不会平均分配时间给每个线程,而是将时间更多的分配给更重要的线程.这个线程的重要程度是可以由我们所决定的
线程的优先级由一个int表示从1到10优先级依次增加,所有线程的默认优先级为5,可通过以下的方法修改优先级
1 2 3 t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY); t1.setPriority(7 );
但请注意,这个优先级只是我们想CPU的建议,CPU有可能不会听!!!
线程的让步和加入 当一个线程执行到不重要的部分时我们可以选择让这个信号发信号主动要求CPU减少分配的时间.调用下面的方法即可
当然,还有更加直接的,我们可以直接让这个线程停止,等待某个线程执行完毕后再执行,这就是join方法.该方法可以使当前线程休眠,直到某线程完成后再继续执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Main { private static int value = 0 ; public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程1完成" ); }); Thread t2 = new Thread (() -> { try { t1.join(); } catch (InterruptedException e) { throw new RuntimeException (e); } for (int i = 0 ; i < 10000 ; i++) value++; System.out.println("线程2完成" ); }); t1.start(); t2.start(); Thread.sleep(1000 ); System.out.println(value); } }
这段程序有一次得到了改进,相较于上一个改进版本,这一版本取消了死循环,减少了计算资源的浪费
当然join也可以接受一个int作为参数表示最大的等待时间,如果超过该时间等待的线程还没有完成则无视等待继续执行当前的线程
线程锁和线程同步 接下来我们继续寻找方法改进这段代码,引入线程锁的机制.先来谈谈什么是线程锁吧
所谓的线程锁更像是一个箱子,多个线程竞争这个箱子,只有拿到这个箱子的线程才能进行箱子中的操作,我们来看一看实际的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Main { private static int value = 0 ; public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) { synchronized (Main.class) { value++; } } System.out.println("线程1完成" ); }); Thread t2 = new Thread (() -> { for (int i = 0 ; i < 10000 ; i++) { synchronized (Main.class) { value++; } } System.out.println("线程2完成" ); }); t1.start(); t2.start(); Thread.sleep(1000 ); System.out.println(value); }
此时同样能实现正确的输出,且两个线程同时进行,这大大提升了程序的运行速度
当然,synchronized
关键字也可以使用在方法中,标识该方法只能在同一刻由唯一一个线程调用
1 public static synchronized void increment ()
但是你有没有好奇过Main.class
是什么东西.还记得吗,对于Java来说万物都是对象,类难道就不能是对象了吗,当然能.事实上我们所谓的某个类中的静态属性,静态方法都是一个类的class对象中的属性和方法.JVM在加载某个类的时候实质上是生成一个该类的class对象.这一对象全局唯一.我们要操作的value是static的所以我们声明的加锁的对象就是Main类的class对象(这部分内容的具体知识我们将在反射中学习)
所以如果我们要操作的是某个对象的属性,我们选择锁的时候就在括号内添加相应的对象作为锁所属的标识
这里值得一提的是锁并非只有一种,还有两种实现锁的方式,但这里我们先不讲,有兴趣的可以自行了解
死锁 接下来聊一个概念,死锁。我们刚才只讨论了只有一把锁的情况,但是如果存在两把锁,情况会怎么样呢,来看下面这一段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class Main { public static void main (String[] args) throws InterruptedException { Object o1 = new Object (); Object o2 = new Object (); Thread t1 = new Thread (() -> { synchronized (o1){ try { Thread.sleep(1000 ); synchronized (o2){ System.out.println("线程1" ); } } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread (() -> { synchronized (o2){ try { Thread.sleep(1000 ); synchronized (o1){ System.out.println("线程2" ); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); } }
此时我们会发现线程1与线程2永远不会完成,为什么呢.稍微分析以下.假设1与2同时开始运行此时线程1持有o1锁,线程2持有o2锁,但接下来线程1需要获取的o2锁此时被线程2持有,线程2所需获取的o1锁被线程1持有,双方都在持有对方所需要的锁的同时等待对方释放锁以便己方进行下一步,这导致双方都无限期的等待,这种现象就称为死锁.
但是必须要指出的是有时候死锁现象是一个概率事件,不像上面那段程序一样会百分百触发,所以在编写程序时一定要注意出现死锁的可能
当然检查的方法不是没有,在命令行中输入jstack Main
获得当前的进程信息,JVM将会帮我们检查是否存在死锁,获得信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Found one Java-level deadlock: ============================="Thread-0" : waiting to lock monitor 0x00000291f62416c0 (object 0x000000062241eb48 , a java.lang.Object), which is held by "Thread-1" "Thread-1" : waiting to lock monitor 0x00000291f62417a0 (object 0x000000062241eb58 , a java.lang.Object), which is held by "Thread-0" Java stack information for the threads listed above: ==================================================="Thread-0" : at Main.lambda$main$0 (Main.java:10 ) - waiting to lock <0x000000062241eb48 > (a java.lang.Object) - locked <0x000000062241eb58 > (a java.lang.Object) at Main$$Lambda/0x00000291b2003200 .run(Unknown Source) at java.lang.Thread.runWith(java.base@21.0 .4 /Thread.java:1596 ) at java.lang.Thread.run(java.base@21.0 .4 /Thread.java:1583 )"Thread-1" : at Main.lambda$main$1 (Main.java:22 ) - waiting to lock <0x000000062241eb58 > (a java.lang.Object) - locked <0x000000062241eb48 > (a java.lang.Object) at Main$$Lambda/0x00000291b2003420 .run(Unknown Source) at java.lang.Thread.runWith(java.base@21.0 .4 /Thread.java:1596 ) at java.lang.Thread.run(java.base@21.0 .4 /Thread.java:1583 ) Found 1 deadlock.
发现一个死锁,进行处理即可
wait和notify方法 接下来看一看wait和notify方法.这两个方法来自于Object类,是该类中专门用于适配多线程,处理可能得死锁的情况的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Main { public static void main (String[] args) throws InterruptedException { Object o1 = new Object (); Object o2 = new Object (); Thread t1 = new Thread (() -> { synchronized (o1){ try { o1.wait(); Thread.sleep(1000 ); synchronized (o2){ System.out.println("线程1" ); } } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread (() -> { synchronized (o2){ try { Thread.sleep(1000 ); synchronized (o1){ System.out.println("线程2" ); o1.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); } }
此时由于互相等待将不会出现死锁的情况
注意,这两个方法都是只有在拥有对应的锁的情况下才能正常调用的,否则会抛出异常
还有一个方法notifyall()
这个方法会唤醒的是所有的wait()
,而noyify()
在存在多个wait时只会随机的唤醒一个
ThreadLocal 所有的线程共享主内存,而每个线程都又有自身的独立内存,我们是否可以创造一个变量只允许一个线程访问呢,当然可以.这就是ThreadLocal类的作用
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) throws InterruptedException { ThreadLocal<String> local = new ThreadLocal <>(); Thread t1 = new Thread (() -> { local.set("asd" ); System.out.println(local.get()); }); Thread t2 = new Thread (() -> { local.set("yyds" ); System.out.println(local.get()); }); t1.start(); t2.start(); }
此时虽然是同一个变量名但是其值却保存在线程的独立内存中,每个线程都有一个独立的值各个线程都只能获得自身线程的值.当然,如果在线程内再开一个子线程,这个子线程同样无法获取当前线程的值,不过也不是没有办法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Main { public static void main (String[] args) throws InterruptedException { ThreadLocal<String> local = new InheritableThreadLocal <>(); Thread t1 = new Thread (() -> { local.set("asd" ); System.out.println(local.get()); new Thread (()->{ System.out.println(local.get()); local.set("bwnb" ); System.out.println(local.get()); }).start(); try { Thread.sleep(1000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println(local.get()); }); Thread t2 = new Thread (() -> { local.set("yyds" ); System.out.println(local.get()); }); t1.start(); t2.start(); } }
遗传只是值的遗传,二者还是不会共享内存,此后二者的值还是独立的
定时器 如果我们希望将某个数据每隔一段时间就进行一次备份,那么我们直接能想到的方法是创建一个线程,在线程中包含一个循环,这个循环中调用sleep
方法即可,但是更复杂的情况呢?其实有专门的Timer类用来完成这一任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Main { public static void main (String[] args) { Timer timer = new Timer (); timer.schedule(new TimerTask () { @Override public void run () { System.out.println(Thread.currentThread().getName()); } }, 0 ,1000 ); timer.scheduleAtFixedRate(new TimerTask () { @Override public void run () { System.out.println(Thread.currentThread().getName()); } }, 0 ,1000 ); } }
上下两个方法的区别在于第一个方法是在每次任务结束后再计时,保证两次任务之间的间隔一致,而第二个方法则是时刻在计时,保证任务执行的速率或者说每个任务的触发时间点在时间轴上是均匀的
当然,上面的例子还体现了一点,尽管我实际上有两个定时任务,但这两个定时任务其实是运行在一个线程上的每一个Timer对象都只会开启一个新线程
当然,除了定时任务,还有延时任务
1 timer.schedule(task, delay);
这种情况下只会执行一次
如果最后要取消定时任务,直接调用
还需要再强调一点,就是如果你使用了延时任务,那么该任务执行一次后线程不会立刻结束,而是再继续等待一定的时间后无法得到新的任务,自动退出,所以为了避免这种情况,最好在延时任务的末尾直接调用cancle结束任务
守护线程 所谓的守护线程就是专门用来监视其他线程的状态避免出现某种异常而导致其他线程停止运行的一种线程
我们创建一个守护线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) throws InterruptedException{ Thread t = new Thread (() -> { while (true ){ try { System.out.println("守护线程" ); Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.setDaemon(true ); t.start(); for (int i = 0 ; i < 5 ; i++) { Thread.sleep(1000 ); } }
注意,守护线程会在其他所有线程结束时自动结束.
此外,守护线程的子线程默认状态下也是守护的,当然也可以手动设置为非守护
批量数据的多线程处理 多线程的一个显而易见的优势就是效率高,所以我们当然可以用多线程来实现大量数据的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Main { public static void main (String[] args) throws InterruptedException { List<Integer> list = new ArrayList <>(); new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) { list.add(i); } }).start(); new Thread (() -> { for (int i = 1000 ; i < 2000 ; i++) { list.add(i); } }).start(); Thread.sleep(2000 ); System.out.println(list.size()); } }
这是一个简单的实现两个线程同时进行插入的操作,但是这段程序并不是安全的,在运气不好的情况下会出现数组越界的情况,为什么呢
我们都知道实际上ArrayLIst是用数组实现的,它能够实现无限的插入数据的原因是在数组容量不足时会新申请一个更大的数组将原来的数据拷贝给新数组,我们回忆一下原理
1 2 3 4 5 6 private void add (E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1 ; }
这是添加方法的实现,所谓的s就是当前元素的数量,如果元素的数量等于容量则调用grow扩容,但如果是多线程,就有可能两个线程同时走到了这一步,此时又恰好刚好里数组上限差一个,那么检查时就不会进行扩容,可接下来会插入两个数造成数组越界,
所以我们应当涉及适当的锁机制避免可能的这种情况
还记得吗,集合类中国有几个与并行运算相关的方法,如并行迭代器,并行流,这些方法是保证了线程安全的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Main {private static int sum1=0 ;private static int sum2=0 ; public static void main (String[] args) throws InterruptedException { List<Integer> list = new ArrayList <>(); for (int i = 0 ; i < 1000 ; i++) { list.add(i); } Spliterator<Integer> spliterator1 = list.spliterator(); Spliterator<Integer> spliterator2 = spliterator1.trySplit(); new Thread (() -> { spliterator1.forEachRemaining(a->sum1+=a); System.out.println(sum1); }).start(); new Thread (() -> { spliterator2.forEachRemaining(a->sum2+=a); System.out.println(sum2); }).start(); } }
这些操作显然有些智障,但如果LIst是一个随机的列表呢,这样两个线程工作就快了很多
不过我们也可以进一步改进我们的工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Main {private static int sum1=0 ; public static void main (String[] args) throws InterruptedException { List<Integer> list = new ArrayList <>(); for (int i = 0 ; i < 1000 ; i++) { list.add(i); } list.parallelStream().forEach(a->{ if (a!=null ){ sum1+=a; } }); System.out.println(sum1); } }
这样可以简单的实现并行运算(实际上并行流也是用可拆分迭代器实现的)
在多线程状态下,我们前面所学到的集合类都是不安全的,容易出现各种意外.为了避免这种意外的出现,我们需要引入其他线程安全的类,不过这是后话了,我们会在将来继续介绍这些东西
结语 多线程的内容远不是这一点文字内容所能概括的,将来会更详细的讲解,不过暂时还是到此为止吧,最好能够把我上面提到的所有的代码都独立的实现一次观察效果,当然有些代码不是那么完善,你也可以做一定的改进
上图