五、线程死锁问题
1、介绍死锁问题及实例情况
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
Java语言通过synchronized关键字来保证原子性,其中每一个Object都有一个隐含锁,这个也称为监视器对象,在进入到synchronized之前自动获取此内部锁,一旦离开此方式会自动释放锁。
使用1个例子来描述死锁如何形成:
public class Main { public static void main(String[] args) throws InterruptedException { StringBuffer buffer1 = new StringBuffer(); StringBuffer buffer2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (buffer1){ buffer1.append("A"); buffer2.append("A"); //睡眠2秒 sleep2Sec(); synchronized (buffer2){ buffer1.append("B"); buffer2.append("B"); } } System.out.println("线程1中:buffer1="+buffer1); System.out.println("线程1中:buffer2 = "+buffer2); } }.start(); new Thread(){ @Override public void run() { synchronized (buffer2){ buffer1.append("C"); buffer2.append("C"); //睡眠2秒 sleep2Sec(); synchronized (buffer1){ buffer1.append("D"); buffer2.append("D"); } } System.out.println("线程2中:buffer1="+buffer1); System.out.print("线程2中:buffer2 = "+buffer2); } }.start(); } //延时2秒 public static void sleep2Sec(){ //增加延时 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }
注意看两个线程中的run()方法里有两个synchronized同步代码块,第一个线程run()中第一个同步代码块将buffer1作为锁,第二个线程的第一个同步代码块将buffer2对象作为锁,两个同步代码块的内部同步代码块也是各自设置buffer1或buffer2为锁。
为了造出死锁的情况,在两个线程都进入到第一个同步代码块时都使用sleep()来让线程阻塞一会(不会释放锁),待两个线程都同时进入到第一个同步代码块中,第一个线程拿到buffer1内部锁,第二个线程拿到buffer2内部锁,一旦sleep()结束阻塞,那么就会出现死锁状况,各个都在等待对方资源被释放。
即不断的在阻塞中…
这个例子是因为多线程访问共享资源由于访问顺序原因所造成阻塞情况,一个线程锁住资源A,由想去锁住资源B;在另一个线程中先锁住B,又想锁住A来完成操作,一旦两个线程同时先后锁住A与B时,就会造成两个线程都在等待情况,程序进入阻塞。
2、解决与避免死锁
通过专门的算法,尽量避免同不资源定义以及避免嵌套同步。
这里介绍三个技术避免死锁问题:
加锁顺序(线程按照一定的顺序上锁)
加锁时限(线程尝试获取锁时加上一定时限,超过时限则放弃对该锁请求,并释放自己所占有锁)
死锁检测
方式一:加锁顺序
//第一个线程 synchronized (buffer1){ .... synchronized (buffer2){ ..... } //第二个线程 synchronized (buffer1){ .... synchronized (buffer2){ ..... }
说明:对上锁的顺序作适当排序,这样就不会进入到死锁情况,因为无论哪个线程先进入,另一个线程会一直等待锁的释放,直到第一个使用该锁的释放再进行。
方式二:加锁时限
介绍:就是在获取锁的时候加一个超时时间,一旦超过了这个时限则会放弃该锁的请求,并释放自己所占用的锁。
在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用java.util.concurrent包下的工具
六、线程的通信
1、认识线程通信
引出线程通信
为什么要线程通信?
多个线程并发执行时默认是根据CPU调度策略随机分发时间片,对于任务的执行其实是随机的,当我们需要多线程来共同完成一件事,并且希望它能够有规律的执行,那么就需要一些协同通信,来达到多线程共同操纵一份数据。
多线程中若是我们不使用线程通信的方式也是可以实现共同完成一件事,但是在很大程度上多线程会对共享变量进行争夺造成损失,所以引出线程通信,目的是能让多线程之间的通信避免同一共享资源的争夺。
什么是线程通信?
多个线程在处理同一个共享变量,且任务不同时需要线程通信来解决对一个变量使用与操作的随机性,使其变得有规律,具有可控性,避免对同一共享变量进行争夺。
想要实现线程通信这里就引出等待唤醒机制,如wait()、notify()、notifyAll()方法。
认识三个方法,三个方法都是Object对象提供。
Object声明三个方法原因:这三个方法必须由锁对象调用,而任何对象都可以作为synchronized的同步锁。
前提:三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报IllegalMonitorStateException异常(如果当前线程不是此对象的监视器所有者)。
wait():让当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,此时当前线程会进行排队等候其他线程调用notify()与notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能够执行。
简述:该线程暂停等待,释放此监视器的所有权(指锁),等待指定方法唤醒重新获得监视器所有权,从断点处开始执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
简述:唤醒正在等待对象监视器的单个线程,如之前使用wait()方法的线程,需要注意不能唤醒sleep()的线程。
notifyAll():唤醒正在排队等待资源的所有线程结束等待。
简述:唤醒所有等待对象监视器的线程,如通过调用wait()方法之一等待对象的监视器,非sleep()方法。
注意:
这几个方法配合使用需要使用同一个对象来进行调用。
调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)。
2、线程通信小例子(交替打印1-100)
不使用wait()、notify()实现线程通信(不推荐)
这里仅使用继承Thread方式来实现线程通信:
class MyThread extends Thread{ private static int i; @Override public void run() { while(true){ synchronized (MyThread.class){ //指定只有偶数情况且当前线程为线程一时才执行 if(i<100 && i%2 == 0){ if(Thread.currentThread().getName() == "线程一"){ i++; System.out.println(Thread.currentThread().getName()+":"+i); } }else if(i<100 && i%2 == 1){//指定只有奇数情况且当前线程为线程二时才执行 if(Thread.currentThread().getName() == "线程二"){ i++; System.out.println(Thread.currentThread().getName()+":"+i); } } if(i>=100){ break; } } } } } public class Main { public static void main(String[] args) throws InterruptedException { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); thread1.setName("线程一"); thread2.setName("线程二"); thread1.start(); thread2.start(); } }
通过双重判断来达到线程交替打印,不过这种方式会有大量无效情况以及可能会出现问题,更消耗资源。
使用wait()、notify()实现线程通信(推荐)
【1】实现Runnable接口方式
class MyRunnable implements Runnable{ private int i; @Override public void run() { while(true){ //同步代码块 synchronized (this){ if(i<100){ //进行唤醒排队等待监视器的线程,此时继续向下执行相应操作 this.notify(); i++; System.out.println(Thread.currentThread().getName()+":"+i); //进入等待 try { if(i<100) //加一个判断防止最后出现阻塞情况 this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } } public class Main { public static void main(String[] args) throws InterruptedException { MyRunnable runnable = new MyRunnable(); Thread thread1 = new Thread(runnable, "线程一"); Thread thread2 = new Thread(runnable, "线程二"); thread1.start(); thread2.start(); } }
这里因为是实现runnable接口,在创建多个线程时使用的是同一个Runnable实现类,所以我们可以直接使用该对象作为监视器。
【2】继承Thread方式
class MyThread extends Thread{ private static int i; @Override public void run() { while(true){ //将MyThread.class作为锁,只有一个类 synchronized (MyThread.class){ //作为锁的类调用唤醒方法 MyThread.class.notify(); if(i<100){ i++; System.out.println(Thread.currentThread().getName()+":"+i); try { if(i<100) MyThread.class.wait();//释放监视器并进行等待 } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } } public class Main { public static void main(String[] args) throws InterruptedException { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); thread1.setName("线程一"); thread2.setName("线程二"); thread1.start(); thread2.start(); } }
对于继承Thread来实现多线程的,共享变量为static,这里锁为自定义类的class类
3、经典例题(生产者与消费者)
介绍操作系统中的生产者与消费者
操作系统中的问题:系统中有一组生产者进行与一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用(这里"产品"理解为某种数据)。
①生产者、消费者共享一个初始为空、大小为n的缓冲区。
②只有缓冲区没满时,生产者才能把产品放入缓冲区中,否则必须等待;
③只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。
④缓冲区是临界资源,葛金城必须互斥地访问。
模拟生产者与消费者案例
案例描述:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
出现问题描述:
生产者比消费者快时,消费者会漏掉一些数据没有取到。
消费者比生产者快时,消费者会取相同的数据。
程序如下:
//店员类:有生产产品与消费产品功能 class Clerk{ //设置初始产品为0,最高产品数量为20 private int product; //生产产品 public synchronized void addProduct(){ //产品数量够了 if(product>=20){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ product++;//生产产品 System.out.println("产品+1,当前产品数量为"+product); notify();//唤醒操作说明生产了新的产品了 } } //消费产品 public synchronized void consumeProduct(){ //如果产品为0了,那么无法进行消费 if(product<=0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ product--; System.out.println("产品-1,当前产品数量为"+product); notify(); } } } //生产者 class Product extends Thread{ private Clerk clerk; public Product(Clerk clerk) { this.clerk = clerk; } @Override public void run() { //不断进行生产操作 while(true){ //为了让效果更加明显这里对线程使用延时 try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } clerk.addProduct(); } } } //消费者 class Consumer extends Thread{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { //不断进行消费操作 while(true){ clerk.consumeProduct(); } } } public class Main { public static void main(String[] args) throws InterruptedException { Clerk clerk = new Clerk(); Product proThread = new Product(clerk); Consumer conThread = new Consumer(clerk); proThread.start(); conThread.start(); } }
程序分析:
Clerk店员类来负责生产产品与消费产品的进行
生产方法:一旦产品数量>=20,则进入阻塞状态(表示已满无法生产);若<=20就进行生产,并唤醒使用wait()等待的消费者(通知它我生产出产品了你可以进行消费了)。
消费方法:一旦产品数量<=0,则进入阻塞状态(表示无产品暂时无法消费);若>0则进行消费,并唤醒使用wait()等待的生产者(告知它我已经消费产品了,快去生产)。
Product作为生产者线程,Consumer作为消费者线程。
这里给生产者线程加了sleep()方法,生产速递慢,消费速度快,体现的更加明显。
七、JDK5.0新增线程创建方式
方式一:实现Callable接口
介绍Callable接口
Callable接口:与使用Runnable相比, 其功能更加强大
相比run()方法,其实现的callable接口方法call()中可以有返回值
方法可抛出异常。
支持泛型的返回值。
需要借助FutureTask类的get()方法获取call()的返回值。
案例演示
案例描述:使用多线程获取到0-99和的值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { int sum = 0; for(int i = 0;i<100;i++){ sum+=i; } return sum; } } public class Main { public static void main(String[] args) throws InterruptedException { MyCallable callable = new MyCallable(); //将MyCallable创建的实例放置到FutureTask的有参构造器中,futureTask实现了Run()方法,其中就调用了callable的call()方法 FutureTask<Integer> futureTask = new FutureTask<>(callable); //启动线程 new Thread(futureTask).start(); try { //通过FutureTask类的get()方法调用获取call()方法的返回值 Integer sum = futureTask.get(); System.out.println(sum); } catch (ExecutionException e) { e.printStackTrace(); } } }
FutureTask实现了RunnableFuture接口,并且该接口又多继承了Runnable, Future 这两个接口。
Future接口的get()方法:能够获取到实现Callable的call()方法的返回值。
为什么要将MyCallable实例放到FutureTask中?
FutureTask中实现了Runnable接口,其中也包含run()方法,run()方法里调用了实现Callable实例类中的call()方法,并且在run()过程中获取到了call()的返回值,使用其中的set()方法赋值到自己类中属性里。所以我们下面也就可以看到使用其类的get()方法获取到了call()的返回值。
为什么将FutureTask实例放到Thread中?
之前也说到了FutureTask实现了runnable接口,符合Thread类中的一个有参构造,一旦调用start()就会执行FutureTask的run()方法。
源码分析一波
首先看一下Callable接口类:
//函数式接口,允许使用Lambda表达式 @FunctionalInterface public interface Callable<V> { //支持自定义泛型返回值,可以抛出异常 V call() throws Exception; }
接着看RunnableFuture类:见下面1.1
//只列举Future<V>接口,runnable接口中只有一个run()抽象方法这里不展开 public interface Future<V> { ... V get() throws InterruptedException, ExecutionException; } //1.2 RunnableFuture接口 多继承Runnable接口以及Future接口 public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); } //1.1实现RunnableFuture接口 (1.2见RunnableFuture接口) public class FutureTask<V> implements RunnableFuture<V> { //使用outcome来接收call()方法返回值 private Object outcome; //有参构造器,使用Callable多态 public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } //看一下run()方法,之后Thread类中使用start()方法会调用该run()方法 public void run() { ... Callable<V> c = callable; if (c != null && state == NEW) { V result; boolean ran; try { //这里调用了之前有参构造器中传入的Callable接口实现类的call()方法,使用V来接收返回值 result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); } if (ran) //调用set()方法将返回值赋值到 set(result); } .... } //set()方法:本身类自己实现 protected void set(V v) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = v;//赋值操作 UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state finishCompletion(); } } //通过调用get()方法获取到返回值:实现Future接口 public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false, 0L); //调用方法返回 return report(s); } //该方法用于返回outcome的值也就是调用call()的返回值 private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s >= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); } }
关注一下其中的run()方法以及get()方法,简单来说该类中run()方法实际上就是调用了实现Callable类的call()方法,get()方法获取到了call()方法的返回值,具体内容见上。
最后看一下Thread的构造器:
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
由于RunnableFuture也实现了Runnable接口,所以能够传入到Thread的构造器中。
上面只是粗略看了一下源码找出了关键信息,对于具体内容实现并没有太过深入了解,仅大概方法调用也有了些思路。
方式二:使用线程池
认识线程池的相关API
线程池背景及好处
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大,我们可以使用现成的线程池。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
提高响应速度(减少了创建新线程的时间)。
降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
便于线程管理,例如:corePoolSize:核心池的大小 maximumPoolSize:最大线程数 keepAliveTime:线程没有任务时最多保持多长时间后会终止。这些都可直接设置。
认识了解线程池相关API
同样是JDK5.0,提供了线程池的相关的API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable。
Future submit(Callable task):执行任务,有返回值,一般又来执行 Callable。
void shutdown() :关闭连接池。
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
ExecutorService Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
ExecutorService Executors.newFixedThreadPool(n): 创建一个可重用固定线程数的线程池
ExecutorService Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
ScheduledExecutorService Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行。
说明:我们主要使用Executors工具类来获取到线程池,上面列举到的前三个实际上返回的是ThreadPoolExecutor这个实现类,ExecutorService是该实现类实现的接口。
我们想要执行我们自定义的线程任务就可以使用上面ExecutorService列举到的方法。
实例:使用线程池创建10个线程来执行指定方法
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyRunnable implements Runnable{ @Override public void run() { for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } } public class Main { public static void main(String[] args) { //1、使用工具类创建10个线程 ExecutorService pool = Executors.newFixedThreadPool(10); //2、在将来某个时候执行给定的任务,这里submit()方法需要提供Runnable接口实现类 pool.submit(new MyRunnable()); //3、启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。 pool.shutdown(); } }
如何使用线程池的属性?
首先列举三个属性:
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
我们需要向下转型为ThreadPoolService才能调用指定方法
查看源码
首先看Executors.newFixedThreadPool(10)方法
//这里实际上使用了多态,ExecutorService是ThreadPoolExecutor的接口 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
再其ExecutorService接口中并没有设置线程池属性的方法:
所以我们需要向下转型,这是允许的,因为在本身返回的时候就是返回的ThreadPoolExecutor实例对象。
相关面试题
1、synchronized与Lock 的对比
相同点:二者都可以解决线程安全问题
不同点:
synchronized机制再执行完相应的同步代码以后,会自动的释放同步监视器。
Lock锁需要手动上锁以及解锁,结束同步需要手动调用unlock()方法。
使用Lock锁,JVM将会花费较少的时间来调度线程,性能会更好,并且具有更好的扩展性(提供了更多的子类)。
优先使用顺序:Lock锁 > 同步代码块(进入方法体分配了相应资源) -> 同步方法
2、sleep()与wait()方法异同点
相同点:这两个方法都能够让线程进入到阻塞状态。
不同点:
两个方法声明不同,sleep()方法声明在Thread类中,wait()方法声明在Object类中。
调用位置不同,sleep()方法在任何需要的场景下都可以使用,而wait()方法只能在同步代码块或同步方法中使用。
关于是否释放监视器,sleep()不会释放锁,wait()会释放锁。