多线程的使用
Posted YL10000
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程的使用相关的知识,希望对你有一定的参考价值。
1 为什么使用多线程
1.1 发挥多核cpu的优势
单核CPU上的多线程是假的多线程,同一时间处理器只会处理一段逻辑,只是在多个线程之间进行快速切换
多核CPU才能实现真正的多线程,同时处理多个逻辑,充分利用CPU
1.2 防止阻塞
单核CPU不仅不能发挥多线程的优势,反而因为多个线程的切换,反而降低整体效率。
但是单核CPU还是要使用多线程,防止阻塞。这样在一个线程卡死以后,不会影响其他线程的正常运行。
1.3 便于建模
对于一个大的而且复杂的任务,可以分为多个小的简单的任务进行程序建模。
2 start()和run()方法的区别
start()方法来启动线程,真正实现多线程。无需等待run()方法体的执行而直接执行下面的代码。
通过调用Thread来的start()方法启动一个线程,此时该线程处于就绪(可运行)状态,但并没有运行,一旦得到cpu时间片,就开始执行run()方法,称为线程体。
run()只是个普通的方法,并不会创建新的线程也不会执行调用线程的代码。
3 CyclicBarrier和CountDownLatch的区别
3.1 CyclicBarrier在某个线程中调用await()方法后,该线程就停止运行,知道所有的线程到到进入到barrier状态,执行完CyclicBarrier中定义的run()方法后再执行该线程之后的代码。
而CountDownLatch,当线程运行到await()时,只要CountDownLatch中的数值减到0,该程序就会继续运行。
3.2 CyclicBarrier的await()只是让当前线程停止运行,而CountDownLatch的await()会让所有的线程都进入停止运行
3.3 CyclicBarrier可重复使用,而CountDownLatch不可重复使用
public static void main(String[] args) { /** * CountDownLatch 的三个重要方法 * await() :调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 * await(long timeout, TimeUnit unit) :和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 * countDown() :将count值减1,如果计数到达零,则释放所有等待的线程。 */ //定义一个计数器,并设置计数器的初始值 final CountDownLatch latch=new CountDownLatch(2); /** * 通过它可以实现让一组线程等待至某个状态之后再全部同时执行。 * 叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。 * 我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。 */ final CyclicBarrier barrier=new CyclicBarrier(2, ()->{ System.out.println("所有到子线都进入到了barrier状态"); }); for(int i=0;i<2;i++){ //定义一个子线程 new Thread(()->{ System.out.println("子线程"+Thread.currentThread().getName()+"开始运行"); try { Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } System.out.println("子线程"+Thread.currentThread().getName()+"运行结束"); //将计数器的值减1 latch.countDown(); System.out.println("子线程"+Thread.currentThread().getName()+"减一之后"); try { Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } System.out.println("子线程"+Thread.currentThread().getName()+"进入到barrier"); try { //让该线程进入到barrier状态,暂停运行,等所有的线程都进入到barrier状态后, //执行barrier中定义的run()方法后运行之后的额代码 barrier.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println("子线程"+Thread.currentThread().getName()+"barrier之后的代码"); }).start(); } System.out.println("主线程"+Thread.currentThread().getName()+"开始运行"); try { System.out.println("等待两个子线程运行结束"); //将主线程挂起,等待计算器的值变为0后再向下执行 latch.await(); //将主线程挂起,等待2000毫秒,如果计数器的值还没有变为0,就向下执行 //latch.await(2000,TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("两个子线程运行结束"); System.out.println("主线程"+Thread.currentThread().getName()+"运行结束"); }
4 volatile关键字
4.1 并发编程的三个概念:原子性,可见性和有序性
4.1.1 原子性:即一个或多个操作要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。
4.1.2 可见性:指多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程就能立即看到修改的值。
4.1.3 有序性:程序执行的顺序按照代码的先后顺序执行。
4.2 java语音本身对并发编程的保证
4.2.1 原子性
在java中对基本数据类型的变量的读取和赋值操作是原子性操作的(简单的读取和赋值,变量间的赋值不是原子性的)
java1.5之前可用通过Syschronized和Lock来实现,java1.5之后可以通过java.util.concurrent.atomic包下提供了一些原子操作类实现
4.2.2 可见性
通过volatitle实现可见性:
被volatitie修饰的变量修改后会立即更新到主存中,其他线程需要读取时会从主存中读取
而普通变量不能保证可见性的原因是,修改后什么时候更新到主存中是不确定的
通过Syschronized和Lock保证可见性
保证同一时刻只有一个线程获取锁然后执行同步代码,并在释放锁之前将变量的修改刷新到主存中
4.2.3 有序性
在java内存模型中,允许编译器和处理器对指令进行重排序,但重排序不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。
volatitle可以保证一定的有序性,但要保证完整的有序性还是需要使用Syschronized和Lock来实现
java内存具有一些先天的有序性,即不通过任何手段就能保证的有序性,称为“happens-before原则”(先行发生原则)
4.3 volatitle对并发编程三特性的影响
4.3.1 可见性 保证了不同线程对共享变量的可见性
4.3.2 原子性 volatitle不能保证对变量的任何操作都是原子性的
4.3.3 有序性 禁止了指令重排序
当执行到volatitle变量的读取和写操作时,其前面的操作肯定是全部执行完了,且结果已经对后面的操作可见,而在此后面的代码肯定没有执行到
4.4 volatitle使用条件:保证原子性操作,才能保证使用volatitle关键字的程序在并发时能够正确执行
4.4.1 对变量的写操作不依赖于当前值
4.4.2 该变量没有包含在具有其他变量的不变式中
5 sleep()和wait()的区别
两者都可以让程序放弃CPU一段时间,不同点在于如果线程持有某个对象的监视器,sleep不会放弃这个对象的监视器,而wait()会放弃这个对象的监视器。
6 ThreadLocal的作用
使用ThreadLocal维护变量时,ThreadLocal为每个使用改变了的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程中的副本。
7 wait()和notify()/notifyAll()在放弃对象监视器的区别
wait()会立即释放对象监视器
notify()/notifyAll()会等待线程剩余代码执行完毕后才会释放对象监视器
Integer lock=3; synchronized (lock) { //仅当对象obj的监视器被当前线程持有的时候才会返回true boolean b=Thread.holdsLock(lock); System.out.println("该线程拥有该对象监视器:"+b); }
8 死锁
Integer a=3; Integer b=4; new Thread(()->{ synchronized (a) { System.out.println("线程"+Thread.currentThread().getName()+"获取到a锁,正在等待b锁"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("线程"+Thread.currentThread().getName()+"获取到b锁"); } } }).start(); new Thread(()->{ synchronized (b) { System.out.println("线程"+Thread.currentThread().getName()+"获取到b锁,正在等待a锁"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (a) { System.out.println("线程"+Thread.currentThread().getName()+"获取到a锁"); } } }).start(); }
9 Thread.sleep(0)的作用
由于java采用抢占式的线程调度算法,为了让优先级比较低的线程也能获取到CPU控制权,使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,来平衡CPU控制权。
以上是关于多线程的使用的主要内容,如果未能解决你的问题,请参考以下文章