-
1.Java JUC简介
-
多线程的目的
- (1).提高效率。尽可能去利用cpu和系统资源。
- (2).注意:如果多线程使用不当的话,不仅不能提高效率,反而性能会更低,因为多线程的开销实际上比线程要大,因为多线程涉及线程之间的调度,以及cpu上下文切换,以及线程的创建,销毁,线程同步等问题。而单线程不涉及这些问题
-
JUC包
- JDK1.5以后,Java为我们提供了java.util.concurrent包,这个包里面提供了大量应用于线程的一些工具类,供我们在不同需求上进行应用。
-
多线程的目的
-
2.volatile关键字-内存可见性
-
(1).回顾:下列描述的程序中,flag为多线程运行环境下的共享实例变量。main线程在此程序中的作用是是读取flag标志的值,Thread匿名线程负责修改flag线程的值,而此时运行发现两个线程读取或修改的值出现了不一致的情况。
public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start();//此匿名线程作用是改值 //main线程的作用是轮询读值 while(true){ if(td.isFlag()){ System.out.println("---------"); break; } } } } class ThreadDemo implements Runnable{ private boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
- 运行结果
-
- 从运行结果来看,匿名线程执行时,将flag的值由默认的false设置成了true并成功读取后打印,但主函数中main线程轮询读取到的flag的值一直为默认的false,轮询不结束,程序就一直运行着不结束。可见,匿名线程与main线程在此处出现了读取flag变量不一致的情况。
- (2)问题产生的原因:这是由Java内存可见性引起的问题。具体为两个线程在操作共享数据(共享实例变量)时,对共享数据的操作彼此不可见。(图)
-
(3).解决方式:
-
①使用synchronized同步锁改进。
public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start();//此匿名线程作用是改值 //main线程的作用是轮询读值 while(true){ synchronized (td){ if(td.isFlag()){ System.out.println("---------"); break; } } } } } class ThreadDemo implements Runnable{ private boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
运行结果: -
- main线程与匿名线程读取共享变量flag值的结果一致,轮询停止,程序结束。
- 此种解决方式的缺点:效率低,可能发生多个线程阻塞现象。
-
②使用volatile关键字解决内存可见性问题。
-
volatile关键字的作用:保证多个线程访问共享数据时,彼此的数据在内存中是可见的,读取一致。
public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start();//此匿名线程作用是改值 //main线程的作用是轮询读值 while(true){ if(td.isFlag()){ System.out.println("---------"); break; } } } } class ThreadDemo implements Runnable{ private volatile boolean flag = false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
-
- 运行结果:
-
- main线程与匿名线程读取共享变量flag值的结果一致,轮询停止,程序结束。
- 此种解决方式的缺点:直接从主存中读数据,效率会降低,但比锁的效率高。但存在原子性问题。
-
注意:
- volatile不具备互斥性
- volatile不能保证变量的变量的原子性
-
volatile关键字的作用:保证多个线程访问共享数据时,彼此的数据在内存中是可见的,读取一致。
-
①使用synchronized同步锁改进。
-
(1).回顾:下列描述的程序中,flag为多线程运行环境下的共享实例变量。main线程在此程序中的作用是是读取flag标志的值,Thread匿名线程负责修改flag线程的值,而此时运行发现两个线程读取或修改的值出现了不一致的情况。
-
3.原子变量-CAS算法(无锁算法)
-
(1)引入问题:i++操作的原子性问题
public class TestAtomic { public static void main(String[] args) { AtomicDemo ad = new AtomicDemo(); //创建10个线程调用i++方法 for(int i = 0; i < 10; i++){ new Thread(ad).start(); } } } class AtomicDemo implements Runnable{ private volatile int seriableNumber = 0; @Override public void run(){ try { Thread.sleep(10000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + getSeriableNumber()); } public int getSeriableNumber(){ return seriableNumber++; } }
- 运行结果:
-
i= i++操作在计算机底层的实现操作: "读-改-写"
- 当有两个线程同时进行i++操作的时候,就容易出现进行了读、改两步操作而还没来得及写的时候,就会出现如上所示的原子性问题。
-
(2).问题解决方案:原子变量
- 概述:jdk1.5后java.util.concurrent包下提供了常用的原子变量。
-
作用:
- 1.原子变量都用volatile修饰,保证了内存可见性。
-
2.CAS(Compare-And-Swap)算法,保证了数据的原子性。
- CAS算法是硬件对于并发操作共享数据的支持。
- CAS包含了三个操作数:内存值V,预估值A,更新值B
- 当且仅当内存值V等于预估值A时,才把更新值B赋值给内存值V,否则,将不做任何操作。
- CAS的步骤: 1.读取内存值V 2.比较和替换(先比较再替换 )。 且1和2之间是同步的,每次只能进行1或2。(有图)
- 3.说白了CAS算法就是当多个线程并发对主存中的数据进行修改的时候,有且只有一个线程会成功,其他线程都会失败。
- 4.CAS算法的效率比synchronized同步锁的效率高,因为当此次不成功时,下一次不会阻塞,也就是说不会放弃cpu给它的执行权,而是紧接着再去尝试更新。
-
代码实现:
import java.util.concurrent.atomic.AtomicInteger; public class TestAtomic { public static void main(String[] args) { AtomicDemo ad = new AtomicDemo(); //创建10个线程调用i++方法 for(int i = 0; i < 10; i++){ new Thread(ad).start(); } } } class AtomicDemo implements Runnable{ //private volatile int seriableNumber = 0; //使用原子变量解决原子性问题 private AtomicInteger seriableNumber = new AtomicInteger(); @Override public void run(){ try { Thread.sleep(10000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + getSeriableNumber()); } public int getSeriableNumber(){ return seriableNumber.getAndIncrement(); } }
运行结果: - i++原子性问题得到解决
- 缺点: 自己要写的算法比较复杂。
-
(3)模拟CAS算法的实现
public class TestCompareAndSwap { public static void main(String[] args) { final CompareAndSwap cas = new CompareAndSwap(); //10个线程分别修改value变量的值,若修改成功则打印true,修改失败则打印false for(int i = 0; i < 10; i++){ new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } int expectedValue = cas.get(); boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101)); System.out.println(Thread.currentThread().getName() + "修改value的值:" + b); } }).start(); } } } class CompareAndSwap{ private int value; //获取内存值 public synchronized int get(){ return value; } //比较 //expectedValue预估值,newValue新值 public synchronized int compareAndSwap(int expectedValue, int newValue){ int oldValue = value; //compare if(oldValue == expectedValue){ this.value = newValue; } return oldValue; } //设置 public synchronized boolean compareAndSet(int expectedValue, int newValue){ return expectedValue == compareAndSwap(expectedValue, newValue); } }
运行结果:
-
(1)引入问题:i++操作的原子性问题
-
4.ConcurrentHashMap锁分段机制
-
(1)概述
- ①jdk1.5以后,java.util.concurrent包中提供了多种并发容器来改进同步容器的性能。
- ②ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对与多线程的操作,介于HashMap 与Hashtable 之间。内部采用“锁分段”机制替代Hashtable 的独占锁。进而提高性能。
- ③java.util.current包还提供了设计用于多线程上下文中的Collection 实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和CopyOnWriteArraySet。当期望许多线程访问一个给定collection 时,ConcurrentHashMap 通常优于同步的HashMap,ConcurrentSkipListMap 通常优于同步的TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的ArrayList。
-
(2)实现:
- concurrentLevel:分段级别。默认值为16,也就是有16个段(Segment),每个段中有默认16个元素,每个元素都是一个链表(其实就是每个Segment下都是一个哈希表)。好处是每个段都是一个独立的锁,意味着当多个线程并发访问ConcurrentHashMap时,可以实现并行的效率。
-
补充:并发容器类CopyOnWriteArrayList 的使用。(注意:添加操作多时效率较低,因为每次添加时都会进行复制,开销会比较大)(并发迭代操作多时可以选择,可以提高效率)
import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; /** * CopyOnWriteArrayList / CopyOnWriteArraySet: 写入并复制 */ public class TestCopyOnWriteArrayList { public static void main(String[] args) { HelloThread ht = new HelloThread(); for(int i = 0; i < 10; i++){ new Thread(ht).start(); } } } class HelloThread implements Runnable{ //此方式会引发并发修改异常 // private static List<String> list = Collections.synchronizedList(new ArrayList<String>()); //此方式解决了上面引发并发修改异常的问题 private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); static{ list.add("Aa"); list.add("BB"); list.add("CC"); } @Override public void run() { //边迭代边生元素 Iterator<String> it = list.iterator(); while(it.hasNext()){ System.out.println(it.next()); //每次写入时都会复制 list.add("aa"); } } }
-
(1)概述
-
5.CountDownLatch闭锁
-
概述:
- 1.Java 5.0 在java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。
- 2.CountDownLatch : 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
- 3.CountDownLatch : 闭锁,在完成某些运算时,只有其他所有线程的运算*全部完成,当前运行才继续执行。
-
使用: 计算10个线程执行完需要的时间
import java.util.concurrent.CountDownLatch; /** * CountDownLatch : 闭锁,在完成某些运算时,只有其他所有线程的运算 * 全部完成,当前运行才继续执行。 * 例如,计算10个线程执行完,需要多长时间。 */ public class TestCountDownLatch { public static void main(String[] args) { //5表示维护的变量,有5个线程操作,每有一个线程完成,递减1 final CountDownLatch latch = new CountDownLatch(5); LatchDemo ld = new LatchDemo(latch); long start = System.currentTimeMillis(); //开启10个线程 for(int i = 0; i < 5; i++){ new Thread(ld).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("耗费时间:" + (end - start) + "毫秒"); } } class LatchDemo implements Runnable{ private CountDownLatch latch; public LatchDemo(CountDownLatch latch){ this.latch = latch; } @Override public void run() { synchronized (this){ try { for (int i = 0; i < 50000; i++) { if (i % 2 == 0) { System.out.println(i); } } }finally{ //减1操作一定要执行,所以放在finally latch.countDown(); } } } }
运行结果:
-
概述:
-
6.实现Callable接口的方式来实现多线程
-
(1)回顾 : 创建执行线程的方式(4种)
- ①继承Thread类
- ②实现Runnable接口
- ③实现Callable接口
- ④利用线程池创建
-
(2)实现Callable接口的方式创建线程 : 相较于实现Runnable接口的方式,方法可以有返回值,并且可以抛出异常。
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 实现Callable接口的方式创建线程 * * 注意:Callable接口方式创建的线程,需要FutureTask实现类的支持,用于接收运算结果。 */ public class TestCallable { public static void main(String[] args) { CallableDemo cd = new CallableDemo(); //1.Callable接口方式创建的线程,需要FutureTask实现类的支持,用于接收运算结果。 FutureTask<Integer> result = new FutureTask<Integer>(cd); new Thread(result).start(); System.out.println("---------------------"); //2.接收线程运算后的结果 try { //线程运算过程中,结果的运算没有执行,有点类似于闭锁 Integer sum = result.get();//FutureTask也可用于闭锁的操作 System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class CallableDemo implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; //计算1-100的值,并将结果返回 for(int i = 0; i <= 100; i++){ System.out.println(i); sum += i; } return sum; } }
运行结果: -
- 注意:Callable接口方式创建的线程,需要FutureTask实现类的支持,用于接收运算结果。FutureTask是Future接口的实现类。
-
(1)回顾 : 创建执行线程的方式(4种)
-
7.Lock同步锁
-
(1)概述:用于解决多线程安全问题的3种解决方案
- ①同步代码块(synchronized隐式锁)
- ②同步方法(synchronized隐式锁)
- ③同步锁(jdk1.5后)(显示锁)(通过lock()方法上锁)(通过unlock()方法进行释放锁)
-
(2)Lock同步锁解决多线程安全问题(多窗口卖票案例)
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TestLock { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(ticket,"1号窗口").start(); new Thread(ticket,"2号窗口").start(); new Thread(ticket,"3号窗口").start(); } } class Ticket implements Runnable{ private int tick = 100; private Lock lock = new ReentrantLock(); @Override public void run(){ while(true){ lock.lock();//上锁 try{ if(tick > 0){ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "完成售票,余票为" + --tick); } }finally{ lock.unlock();//释放锁 } } } }
运行结果: - 没有出现负数票
- (3)生产者消费者案例
-
使用前言: 使用synchronized锁机制实现等待-唤醒机制(生产者消费者案例)
-
①不使用等待唤醒机制的问题 : 产品满时生产者仍不断生产产品。反过来,消费者发现没有产品了,还不断去消费。
public class TestProductorAndConsumer { public static void main(String[] args) { Cleck cleck = new Cleck(); Productor pro = new Productor(cleck); Consumer cus = new Consumer(cleck); new Thread(pro, "生产者A").start(); new Thread(cus, "消费者B").start(); } } //店员 class Cleck{ private int product = 0; //加锁前,卖货和进货两个方法访问的是共享数据,两个方法都存在多线程安全问题 //进货 public synchronized void get(){ if(product >= 10){ System.out.println("产品已满!"); }else{ System.out.println(Thread.currentThread().getName() + ":" + ++product); } } //卖货 public synchronized void sale(){ if(product <= 0){ System.out.println("满货"); }else{ System.out.println(Thread.currentThread().getName() + " : " + --product); } } } //生产者 class Productor implements Runnable{ private Cleck cleck; public Productor(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.get(); } } } //消费者 class Consumer implements Runnable{ private Cleck cleck; public Consumer(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.sale(); } } }
运行结果: - 产品满时生产者仍不断生产产品
- 消费者发现没有产品了,还不断去消费
-
②解决方式:使用等待唤醒机制解决多消费或多生产的问题。(此时仍存在问题,因为我们以后都是在网络中进行,所以我们在下一步加上睡眠来模拟网络延迟)
public class TestProductorAndConsumer { public static void main(String[] args) { Cleck cleck = new Cleck(); Productor pro = new Productor(cleck); Consumer cus = new Consumer(cleck); new Thread(pro, "生产者A").start(); new Thread(cus, "消费者B").start(); } } //店员 class Cleck{ private int product = 0; //加锁前,卖货和进货两个方法访问的是共享数据,两个方法都存在多线程安全问题 //进货 public synchronized void get(){ if(product >= 10){ System.out.println("产品已满!"); try { this.wait(); } catch (InterruptedException e) { } }else{ System.out.println(Thread.currentThread().getName() + ":" + ++product); this.notifyAll(); } } //卖货 public synchronized void sale(){ if(product <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ System.out.println(Thread.currentThread().getName() + " : " + --product); this.notifyAll(); } } } //生产者 class Productor implements Runnable{ private Cleck cleck; public Productor(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.get(); } } } //消费者 class Consumer implements Runnable{ private Cleck cleck; public Consumer(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.sale(); } } }
运行结果: - 此时不再出现产品满时生产者仍不断生产产品,或者消费者发现没货时还不断去消费的问题。但此程序是理想状态下的程序,而我们平时开发应用的时候,往往会有网络延迟,让我们接下来来使用sleep函数来模拟网络延迟,看看程序的执行结果如何。
-
③加上睡眠模拟网络延迟,并减小产品空位。此时的问题:程序不停止。(等待了但被唤醒不了),此时应该去掉else
public class TestProductorAndConsumer { public static void main(String[] args) { Cleck cleck = new Cleck(); Productor pro = new Productor(cleck); Consumer cus = new Consumer(cleck); new Thread(pro, "生产者A").start(); new Thread(cus, "消费者B").start(); } } //店员 class Cleck{ private int product = 0; //加锁前,卖货和进货两个方法访问的是共享数据,两个方法都存在多线程安全问题 //进货 public synchronized void get(){ if(product >= 1){ System.out.println("产品已满!"); try { this.wait(); } catch (InterruptedException e) { } }else{ System.out.println(Thread.currentThread().getName() + ":" + ++product); this.notifyAll(); } } //卖货 public synchronized void sale(){ if(product <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ System.out.println(Thread.currentThread().getName() + " : " + --product); this.notifyAll(); } } } //生产者 class Productor implements Runnable{ private Cleck cleck; public Productor(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.get(); } } } //消费者 class Consumer implements Runnable{ private Cleck cleck; public Consumer(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ try { Thread.sleep(200); } catch (InterruptedException e) { } cleck.sale(); } } }
运行结果:
此时不再出现产品满时生产者仍不断生产产品,或者消费者发现没货时还不断去消费的问题。但是程序不停止,这是由else与多个线程执行造成了某个线程等待了但最后已经没有线程来发出通知唤醒它导致的。- ④去掉else后的版本,程序可以停止了,其余的与上面③的运行结果一致,此处不再演示。
-
上述①-④是1生产者线程和1消费者线程,当我们换成2个消费者线程时,若两个消费者线程同时等待并同时被唤醒那么就会出现负数情况(虚假唤醒)。
public class TestProductorAndConsumer { public static void main(String[] args) { Cleck cleck = new Cleck(); Productor pro = new Productor(cleck); Consumer cus = new Consumer(cleck); new Thread(pro, "生产者A").start(); new Thread(pro, "生产者B").start(); new Thread(cus, "消费者C").start(); new Thread(cus, "消费者D").start(); } } //店员 class Cleck{ private int product = 0; //加锁前,卖货和进货两个方法访问的是共享数据,两个方法都存在多线程安全问题 //进货 public synchronized void get(){ if(product >= 1){//为了避免虚假唤醒问题,应该总是使用在循环中 System.out.println("产品已满!"); try { this.wait(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + ":" + ++product); this.notifyAll(); } //卖货 public synchronized void sale(){ if(product <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " : " + --product); this.notifyAll(); } } //生产者 class Productor implements Runnable{ private Cleck cleck; public Productor(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.get(); } } } //消费者 class Consumer implements Runnable{ private Cleck cleck; public Consumer(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ try { Thread.sleep(200); } catch (InterruptedException e) { } cleck.sale(); } } }
- 解决方式,通过查阅api可知,解决虚假唤醒的方法就是把if改为while。
-
⑤改if为while之后的最终版本
public class TestProductorAndConsumer { public static void main(String[] args) { Cleck cleck = new Cleck(); Productor pro = new Productor(cleck); Consumer cus = new Consumer(cleck); new Thread(pro, "生产者A").start(); new Thread(pro, "生产者B").start(); new Thread(cus, "消费者C").start(); new Thread(cus, "消费者D").start(); } } //店员 class Cleck{ private int product = 0; //加锁前,卖货和进货两个方法访问的是共享数据,两个方法都存在多线程安全问题 //进货 public synchronized void get(){ while(product >= 1){//为了避免虚假唤醒问题,应该总是使用在循环中 System.out.println("产品已满!"); try { this.wait(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + ":" + ++product); this.notifyAll(); } //卖货 public synchronized void sale(){ while(product <= 0){ System.out.println("缺货"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " : " + --product); this.notifyAll(); } } //生产者 class Productor implements Runnable{ private Cleck cleck; public Productor(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.get(); } } } //消费者 class Consumer implements Runnable{ private Cleck cleck; public Consumer(Cleck cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ try { Thread.sleep(200); } catch (InterruptedException e) { } cleck.sale(); } } }
运行结果: - 没有出现虚假唤醒现象。
-
①不使用等待唤醒机制的问题 : 产品满时生产者仍不断生产产品。反过来,消费者发现没有产品了,还不断去消费。
-
使用Lock同步锁实现等待唤醒机制
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 生产者(添加和创建数据的线程) 消费者(删除和销毁数据的线程)案例 */ public class TestProductorAndConsumerForLock { public static void main(String[] args) { Cleck1 cleck = new Cleck1(); Productor1 pro = new Productor1(cleck); Consumer1 cus = new Consumer1(cleck); new Thread(pro, "生产者A").start(); new Thread(pro, "生产者B").start(); new Thread(cus, "消费者C").start(); new Thread(cus, "消费者D").start(); } } //店员 class Cleck1{ private int product = 0; //加锁前,卖货和进货两个方法访问的是共享数据,两个方法都存在多线程安全问题 private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); //进货 public void get(){ lock.lock(); try{ while(product >= 1){//为了避免虚假唤醒问题,应该总是使用在循环中 System.out.println("产品已满!"); try { condition.await(); } catch (InterruptedException e) { } } System.out.println(Thread.currentThread().getName() + ":" + ++product); condition.signalAll(); }finally{ lock.unlock(); } } //卖货 public void sale(){ lock.lock(); try{ while(product <= 0){ System.out.println("缺货"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " : " + --product); // this.notifyAll(); condition.signalAll(); }finally{ lock.unlock(); } } } //生产者 class Productor1 implements Runnable{ private Cleck1 cleck; public Productor1(Cleck1 cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ cleck.get(); } } } //消费者 class Consumer1 implements Runnable{ private Cleck1 cleck; public Consumer1(Cleck1 cleak){ this.cleck = cleak; } @Override public void run(){ for(int i = 0; i < 20; i++){ try { Thread.sleep(200); } catch (InterruptedException e) { } cleck.sale(); } } }
运行结果:
-
(1)概述:用于解决多线程安全问题的3种解决方案
-
8.Condition控制线程通信
- 第7点的最后一个案例即是此方式的实现。
-
9.线程按序交替(案例)(互联网公司面试题)
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 编写一个程序,开启3 个线程,这三个线程的ID 分别为A、B、C,每个线程将自己的ID 在屏幕上打印10 遍,要求输出的结果必须按顺序显示。 如:ABCABCABC…… 依次递归 */ public class TestABCAlternate { public static void main(String[] args) { AlternateDemo at = new AlternateDemo(); new Thread(new Runnable() { @Override public void run() { for(int i = 0; i <= 20; i++){ at.loopA(i); } } },"A").start(); new Thread(new Runnable() { @Override public void run() { for(int i = 0; i <= 20; i++){ at.loopB(i); } } },"B").start(); new Thread(new Runnable() { @Override public void run() { for(int i = 0; i <= 20; i++){ at.loopC(i); System.out.println("-----------------"); } } },"C").start(); } } class AlternateDemo{ private int num = 1;//当前线程正在执行的标记 private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); /** * 循环第几轮 */ public void loopA(int totalLoop){ lock.lock(); try{ //1.判断 if(num != 1){ condition1.await(); } //2.打印 for(int i = 1; i <= 1;i++){ System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); } //3.唤醒 num = 2; condition2.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void loopB(int totalLoop){ lock.lock(); try{ //1.判断 if(num != 2){ condition2.await(); } //2.打印 for(int i = 1; i <= 1;i++){ System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); } //3.唤醒 num = 3; condition3.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void loopC(int totalLoop){ lock.lock(); try{ //1.判断 if(num != 3){ condition3.await(); } //2.打印 for(int i = 1; i <= 1;i++){ System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop); } //3.唤醒 num = 1; condition1.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } }
运行结果:
A、B、C交替打印了20次-
10.ReadWriteLock读写锁
-
(1)概述
- ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有writer,读取锁可以由多个reader 线程同时保持。写入锁是独占的。
- ReadWriteLock 读取操作通常不会改变共享资源,但执行写入操作时,必须独占方式来获取锁。对于读取操作占多数的数据结构。ReadWriteLock 能提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性可以完全不需要考虑加锁操作。
-
(2)实现 : 1个线程写,100个线程读
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReadWriteLock; /** * 读写锁 * * 写写/读写 需要互斥 * 读读 不需要互斥 * * 读锁可以让多个读线程并发持有 * */ public class TestReadWriteLock { public static void main(String[] args) { ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo(); new Thread(new Runnable() { @Override public void run() { readWriteLockDemo.set((int)(Math.random() * 101)); } }, "Write").start(); for(int i = 0; i < 100; i++){ new Thread(new Runnable() { @Override public void run() { readWriteLockDemo.get(); } }).start(); } } } class ReadWriteLockDemo{ private int num = 0; private ReadWriteLock lock = new ReentrantReadWriteLock(); //读 允许多个线程并发读 public void get(){ lock.readLock().lock(); try{ System.out.println(Thread.currentThread().getName() + " : " + num); }finally{ lock.readLock().unlock(); } } //写 一次只能有一个线程来读 public void set(int num){ lock.writeLock().lock(); try{ System.out.println(Thread.currentThread().getName()); this.num = num; }finally{ lock.writeLock().unlock(); } } }
运行结果:
-
(1)概述
-
11.线程八锁(工作中常用的八种情况)
- 题目:判断打印结果
-
不同锁下的打印结果
-
(1)两个普通同步方法,两个线程,标准打印,打印结果是? ONE TWO
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getTwo(); } }).start(); } } class Number{ public synchronized void getOne(){ System.out.println("ONE"); } public synchronized void getTwo(){ System.out.println("TWO"); } }
运行结果: -
(2)新增Thead.sleep()给getOne(),打印结果是? ONE TWO
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getTwo(); } }).start(); } } class Number{ public synchronized void getOne(){ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public synchronized void getTwo(){ System.out.println("TWO"); } }
运行结果: -
(3)新增一个普通方法getThree(),并新增一个线程访问,打印结果是?Three ONE TWO
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getTwo(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getThree(); } }).start(); } } class Number{ public synchronized void getOne(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public synchronized void getTwo(){ System.out.println("TWO"); } public void getThree(){ System.out.println("THREE"); } }
运行结果: -
(4)两个普通同步方法,两个Number对象,打印结果是? TWO ONE
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); Number number2 = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number2.getTwo(); } }).start(); } } class Number{ public synchronized void getOne(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public synchronized void getTwo(){ System.out.println("TWO"); } }
-
(5)修改getOne()为静态同步方法,同一个Number对象,打印结果是? TWO ONE
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getTwo(); } }).start(); } } class Number{ public static synchronized void getOne(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public synchronized void getTwo(){ System.out.println("TWO"); } }
- 运行结果:
-
(6)修改getOne()和getTwo()都为静态方法,同一个Number对象,打印结果是? ONE TWO
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number.getTwo(); } }).start(); } } class Number { public static synchronized void getOne() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public static synchronized void getTwo() { System.out.println("TWO"); } }
运行结果: -
(7)修改getOne()为静态同步方法,getTwo()为普通同步方法,两个Number对象,打印结果是? TWO ONE
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); Number number2 = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number2.getTwo(); } }).start(); } } class Number{ public static synchronized void getOne(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public synchronized void getTwo(){ System.out.println("TWO"); } }
运行结果: -
(8)两个静态同步方法,两个Number对象,打印结果是? TWO ONE
public class TestThread8Monitor { public static void main(String[] args) { Number number = new Number(); Number number2 = new Number(); new Thread(new Runnable() { @Override public void run() { number.getOne(); } }).start(); new Thread(new Runnable() { @Override public void run() { number2.getTwo(); } }).start(); } } class Number{ public static synchronized void getOne(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ONE"); } public static synchronized void getTwo(){ System.out.println("TWO"); } }
运行结果:
-
(1)两个普通同步方法,两个线程,标准打印,打印结果是? ONE TWO
-
结论:
- 同一时刻只有一个对象持有锁。
- 当一个对象持有锁的时候,另外一个对象不能拿到锁。
-
关键:
- 非静态方法的锁默认为this,静态方法的锁为Class实例
- 某一时刻内,只能有一个线程持有锁,无论有多少个方法。
-
12.线程池
- (1)为什么要使用线程池 ?
- 传统方式下,当我们需要线程的时候,我们就new一个线程,然后启动,再需要的时候就再new一个线程,当我们任务过多时,如果我们频繁地创建或销毁线程,那么会非常耗费资源。
-
(2)线程池的作用及体系结构
- ①作用 : 线程池提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建与销毁线程的一些额外开销,提高了响应的速度。
-
②体系结构: java.util.concurrent.Executor : 负责线程的使用与调度的根接口。
-
(核心)ExecutorService : 子接口 : 线程池的主要接口
- ThreadPoolExecutor 线程池实现类
-
ScheduledExecutorService 子接口 负责线程的调度
- ScheduledThreadPoolExecutor 实现类 : 继承了ThreadPoolExecutor,实现了ScheduledExecutorService。具备的功能:线程池的功能以及调度的功能。
-
(核心)ExecutorService : 子接口 : 线程池的主要接口
-
(3)工具类:Executors
- ExecutorService newFixedThreadPool() : 创建固定大小的线程池
- ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动更改数量
- ExecutorService newSingleThreadExecutor() : 创建单个线程池,线程池中只有一个线程。
- ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延时或定时的执行任务。
- 使用线程池的好处: 避免了额外的创建和销毁的开销,也提高了响应速度。
-
(4)线程池的使用案例:
-
为实现Runnable接口的线程分配任务
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestThreadPool{ public static void main(String[] args) { //1.创建线程池,线程池中包含5个线程 ExecutorService pool = Executors.newFixedThreadPool(5); ThreadDemo2 tpd = new ThreadDemo2(); //2.为线程池中的线程分配任务 for(int i = 0; i < 10; i++){ pool.submit(tpd); } //3.关闭线程池 pool.shutdown(); } } class ThreadDemo2 implements Runnable{ private int i = 0; @Override public void run() { while(i <= 100){ System.out.println(Thread.currentThread().getName() + " : " + i++); } } }
运行结果: -
为实现Callable接口的线程分配任务
import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class TestThreadPool{ public static void main(String[] args) throws ExecutionException, InterruptedException { //1.创建线程池,线程池中包含5个线程 ExecutorService pool = Executors.newFixedThreadPool(5); List<Future<Integer>> list = new ArrayList<>(); for(int i = 0; i < 10; i++){ Future<Integer> future = pool.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { int sum = 0; for(int i = 0; i <= 100; i++){ sum += i; } return sum; } }); list.add(future); } pool.shutdown(); //计算1-100的值 for(Future<Integer> future : list){ System.out.println(future.get()); } } } class ThreadDemo2 implements Runnable{ private int i = 0; @Override public void run() { while(i <= 100){ System.out.println(Thread.currentThread().getName() + " : " + i++); } } }
运行结果:
-
为实现Runnable接口的线程分配任务
-
13.线程调度
import java.util.Random; import java.util.concurrent.*; public class TestScheduledThreadPool { public static void main(String[] args) throws ExecutionException, InterruptedException { ScheduledExecutorService pool = Executors.newScheduledThreadPool(5); for(int i = 0; i < 10; i++){ //第一个参数为为线程池分配的任务 //第二个参数为延迟的数值 //第三个参数为延迟的单位 此处为秒 Future<Integer> result = pool.schedule(new Callable<Integer>() { @Override public Integer call() throws Exception { int num = new Random().nextInt(100);//产生一个100以内的随机数 System.out.println(Thread.currentThread().getName() + " : " + num ); return num; } },3 , TimeUnit.SECONDS); System.out.println(result.get()); } //平和方式关闭线程池 pool.shutdown(); } }
运行结果: -
-
14.ForkJoinPool 分支/合并框架 工作窃取 (JDK1.7以后出现的)
- (1)概述 : Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join 汇总。
-
(2)实现 :
import org.junit.Test; import java.time.Duration; import java.time.Instant; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.concurrent.RecursiveTask; import java.util.stream.LongStream; /** * 分支合并框架 */ public class TestForkJoinPool { public static void main(String[] args) { Instant start = Instant.now(); ForkJoinPool pool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinSumCaculate(0L, 50000000000L); Long sum = pool.invoke(task); System.out.println(sum); Instant end = Instant.now(); System.out.println("耗费时间为: " + Duration.between(start, end).toMillis()); } @Test public void test1(){ Instant start = Instant.now(); long sum = 0L; for(long i = 0L; i <= 50000000000L; i++){ sum += i; } System.out.println(sum); Instant end = Instant.now(); System.out.println("耗费时间为 :" + Duration.between(start, end).toMillis()) ; } //java8 新特性 @Test public void test2(){ Instant start = Instant.now(); Long sum = LongStream.rangeClosed(0L, 50000000000L) .parallel() .reduce(0L, Long::sum); System.out.println(sum); Instant end = Instant.now(); System.out.println("耗费时间为:" + Duration.between(start, end).toMillis());//1536-8118 } } /* Recursive表示递归,以递归的方式不断把大任务拆分成小任务 */ class ForkJoinSumCaculate extends RecursiveTask<Long>{ private long start; private long end; private static final long THURSHOLD = 10000L; public ForkJoinSumCaculate(long start, long end){ this.start = start; this.end = end; } @Override protected Long compute() { long length = end - start; if(length < THURSHOLD){ long sum = 0L; for(long i = start; i <= end; i++){ sum += i; } return sum; }else{ long middle = (start + end) / 2; ForkJoinSumCaculate left = new ForkJoinSumCaculate(start, middle); left.fork(); ForkJoinSumCaculate right = new ForkJoinSumCaculate(middle + 1, end); right.fork(); return left.join() + right.join(); } } }
-
- 采用“工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
- 相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。
-
14.ForkJoinPool 分支/合并框架 工作窃取 (JDK1.7以后出现的)