线程详解
线程概述
运行一个音乐播放器播放一首歌,音乐播放器就是一个进程,在程序执行时,既有声音的输出,同时还有该歌曲的字幕展示,这就是进程中的两个线程
线程与进程
程序进入内存就变成了进程,进程就是处于运行中的程序
进程特征:
- 独立性:每个进程都有自己的私有地址,一个进程不能直接访问其他进程
- 动态性:进程有自己的生命周期和不同状态,而程序不具备
- 并发性:多个进程可以在单个处理器上并发执行,进程之间互不影响
并发: 进程在cpu中切换执行 并行:进程在cpu上一起执行
对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,
CPU不断地在这些进程之间轮换执行,虽然CPU在多个进程间轮换执行,但是我们感觉到好像有多个进程在同时进行
线程是进程的执行单元,对于绝大多数的应用程序来说,通常仅要求有一个主线程,
但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程(子线程),
每个线程也是相互独立的
线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,
但不拥有系统资源,它与父进程的其他线程共享该进程所有拥有的全部资源
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
多线程的优势
- 进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其他的每个线程的状态
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,提高运行效率
- 线程共享的环境包括:进程代码段、进程的公有数据
- 进程之间不能共享内存,但线程之间共享内存非常容易
- 系统创建进程是需要为该进程重新分配系统资源,但创建线程则代价小得多
线程的创建与启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用是完成一定的任务,实际上就是执行一段程序代码。
Java使用线程执行体来代表这段程序代码。
Ø 继承Thread创建线程
-
定义Thread类的子类,并重写该类的run()方法,run()方法的方法体代表线程需要完成的任务。因此把run方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程。
public class Test2 extends Thread{ // 重写run方法,run方法的方法体就是子线程的执行体 @Override public void run() { for (int i = 0; i < 100; i++) { // 继承Thread类后,从父类继承的getName方法可以获取当前线程的名称 System.out.println("线程名称:"+this.getName()+" "+i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 调用Thread的currentThread()方法获取当前线程对象 // 这里就不能用this来获取name了 System.out.println("线程名称;"+Thread.currentThread().getName()+"="+i); //创建两个子线程,并运行 if (i == 20){ new Test2().start(); new Test2().start(); } } } }
线程是以抢占式的方式运行的,虽然只创建了两个线程实例,实际上有三个线程在运行
(两个子线程,一个主线程main)
通过setName(String name)的方式来为线程设置名称,也可以通过getName的方式来得到线程的名称。
在默认情况下,主线程的名称为main,用户启动的多线程的名称依次为Thread-0,Thread-1,Thread-3..Thread-n
实现Runnable接口创建线程
- 定义Runnable接口的实现类,并重写该接口的run方法
-
创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程
public class Test3 implements Runnable{ @Override public void run() { for(int i = 0;i < 100;i++) { // 实现了Runnable接口的类其本质并不是线程类,因此没有getName方法, // 因此需要通过Thread类来获取当前线程,仅仅是一个任务体,仍需交给Thread去执行 System.out.println("线程名称:"+Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { /*new一个实现了Runnable接口的实例,这个实例不是线程对象 * 不能test3.start()来运行子线程,执行run方法体 * 实际的线程对象要通过new Thread()来获取,只不过对于实现了Runnable接口的实现类的实例 * 作为参数传入到new Thread(test3).start()来执行子线程 * 意义是让线程对象来执行test3实例的run方法体 * */ Test3 test3 = new Test3(); new Thread(test3).start(); new Thread(test3).start(); } }
又因为Runnable是一个函数式接口,所以可以使用lamda表达式来进行代码编写
public class Test4 { public static void main(String[] args) { /* * 用lamda表达式的写法,{}里面写的就是run方法体 * 将runnable传入到 new Thread(runnable,"子线程1")里面,就表示了创建了子线程 * 并执行run方法体,第二个参数是为子线程起名字 * */ Runnable runnable = ()->{ for (int i = 0; i < 100; i++) { System.out.println("线程名字:"+Thread.currentThread().getName()+"="+i); } }; Test4 test4 = new Test4(); new Thread(runnable,"子线程1").start(); new Thread(runnable,"子线程2").start(); } }
通过对比上面两种创建线程的方式,继承Thread 和 实现Runnable接口,第一种主线程和子线程
分别执行一遍任务。第二种主线程和子线程共同完成一个任务。
Ø 使用Callable&Future创建线程
在Java 1.5开始,Java提供了Callable接口,该接口实际上可看成是Runnable接口的增强版,Callable接口提供了一个call()的方法可以作为线程的执行体,但call()方法比run()方法功能更加强大。
这是因为:
1. call()方法可以有返回值
2. call()方法可以声明抛出异常
因此我们可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。但是存在以下两个问题:
1. Callable接口是Java 5提供的一个新的接口而不是Runnable接口的子接口,所以Callable对象不能直接作为Thread类的target目标执行类。
2. call()方法还有一个返回值——call()方法并不是直接调用,它是作为线程执行体被调用的。如何获取call()方法的返回值?
为了解决以上两个问题,Java 1.5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runable接口——可以作为Thread类的target。

创建并启动有返回值的线程的步骤如下:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且该call()方法有返回值
-
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程
- 调用FutureTask对象的get()方法来获得子线程结束后的返回值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 实现Callable接口时指定的泛型为返回值的类型 */ public class Test5 implements Callable<Integer> { /* * 对于实现了Callable接口的类,重写的call方法就是子线程要执行的方法体 * 这个call方法与run方法的区别就是,call有返回值,可以声明抛出异常 * * 实现的Callable接口可以看做是Runnable接口的增强版,所以可以提供一个Callable对象作为target传给线程对象 * 但是问题就是,Callable接口不是Runnable接口的子接口,不能直接作为target * call方法有返回值 * * */ @Override public Integer call() throws Exception { for (int i = 0; i < 100; i++) { /*与实现了Runnable接口的run方法相似,也是不能使用this来获取name*/ System.out.println("当前线程名称:"+Thread.currentThread().getName()+" "+i); Thread.sleep(200); } return 100; } public static void main(String[] args) throws ExecutionException, InterruptedException { /*创建Callable对象,因为当前类实现了Callable接口 多态*/ Callable<Integer> callable = new Test5(); // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable FutureTask<Integer> futureTask = new FutureTask<>(callable); /*创建线程对象*/ new Thread(futureTask).start(); // 获取线程结束后的返回值 System.out.println("线程执行结束后的返回值:"+futureTask.get()); } }
Callable接口是一个函数式接口,所以可以用lamda表达式写法
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test6 { public static void main(String[] args) throws Exception { /* * Callable接口也是一个函数式接口,所以可以用lamda表达式写法 * {} 里面写的就是call的方法体 * */ Callable<Integer> callable = ()->{ for (int i = 0; i < 100; i++) { System.out.println("线程名称:"+Thread.currentThread().getName()+"="+i); } return 100; }; // 创建FutureTask对象,并将call对象封装在FutureTask内部,FutureTask的泛型为Callable FutureTask<Integer> futureTask = new FutureTask<>(callable); /*创建线程对象 并将FutureTask封装好的Callable对象传入线程对象中*/ new Thread(futureTask).start(); // 获取线程结束后的返回值 System.out.println("线程执行结束后的返回值:"+futureTask.get()); } }
Ø 创建线程的三种方式比较
通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常,并且Callable需要FutureTask来进行封装成Thread可识别的target目标。因此可以将实现Runnable接口和实现Callable接口归纳为一种方式。这种方式与继承Thread方式之间的主要差别如下

线程的声明周期
当线程被创建并启动后,并不是一启动就进入了执行状态,也不是一直处于执行状态,
在线程的生命周期中,它要经历新建、就绪、运行、阻塞和死亡5种状态。
尤其是当线程启动以后,它不可能一直占用CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会在运行、阻塞之间切换。
新建状态:当new了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样仅仅由Java虚拟机为其分配内存,并初始化其他成员变量的值
就绪状态:当线程对象调用了start方法之后,该线程就处于就绪状态,Java虚拟机会为这个线程对象创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于什么时候开始运行,则取决于JVM里的线程调度器的调度。
运行状态:处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程就处于运行状态
阻塞状态:
- 线程调用sleep()方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
-
线程在等待某个通知(notify)
- 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
死亡状态:
- run()或call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的Exception或者直接Error错误
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用
当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动之后,它就拥有和主线程相同的地位
不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程不可以再次作为线程执行。
控制线程
join方法
Thread提供了一个让线程等待另一个线程完成的方法:join()方法。当某个线程执行中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的线程执行完成为止。
public class Test7 extends Thread{ @Override public void run() { System.out.println("准备开会,然后进入等待状态..............."); try { DepartmentOne one = new DepartmentOne(); one.start(); DepartmentTwo two = new DepartmentTwo(); two.start(); DepartmentThree three = new DepartmentThree(); three.start();
one.join();
two.join(); three.join(); } catch (Exception e) { e.printStackTrace(); } /*一二三 三个部门都执行完,才会从阻塞状态进入就绪状态 * 哪个线程用了join方法,那就是要等着这个线程运行结束 * */ System.out.println("人员到齐,开会!!!"); } /*内部类*/ class DepartmentOne extends Thread{ @Override public void run() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门一准备好了"); } } /*内部类*/ class DepartmentTwo extends Thread{ @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门二准备好了"); } } /*内部类*/ class DepartmentThree extends Thread{ @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("部门三准备好了"); } } public static void main(String[] args) { new Test7().start(); } }
程序解析:在主线程启动后,分别开启了三个线程DepartmentOne、DepartmentTwo、
DepartmentThree。三个线程分别进行了启动,并将三个线程加入到主线程Test7中,
主线程会等待三个子线程结束后,才会继续进行
后台线程
有一种线程在后台运行,它的任务是为其他线程提供服务,这种线程被称为“后台线程”或者“守护线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有的前台线程死亡,后台线程就会自动死亡。
调用Thread对象的setDaemon(true)方法可以将指定线程设置为后台线程。
public class Test8 implements Runnable{ @Override public void run() { while(true) { System.out.println("食堂正在营业............."); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } /*内部类*/ public static class Diner implements Runnable{ @Override public void run() { for(int i=0;i<3;i++) { System.out.println("去食堂吃饭........"); try { Thread.sleep(2000); } catch (Exception e) { e.printStackTrace(); } } } } /* * Test8类run方法体是个死循环,Diner内部类是个有限循环 * 前者设置文后台进程,后者正常执行完毕,后台进程也会死亡 * */ public static void main(String[] args) { Thread test8 = new Thread(new Test8()); test8.setDaemon(true); test8.start(); Thread diner = new Thread(new Diner()); diner.start(); } }
线程休眠
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现
线程让步yield
yield()方法是和sleep()方法相似的方法,它也是Thread类提供的一个静态的方法,同样可以让当前线程暂停,但是不阻塞线程而是让当前线程进入就绪状态。
让系统的线程调度器重新调度一次,完全有可能出现的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。
关于sleep()方法和yield()方法的区别:
² sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
² sleep()方法将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
² sleep()方法声明抛出了InterruptedException,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常。
² sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
Ø 改变线程的优先级
每个线程执行都具有一定的优先级,优先级较高的线程会获得更多的执行机会,而优先级较低的线程则获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程优先级相同,在默认的情况下main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
Thread类提供了setPriority(int newPriority),getPriority()方法来设置和获取指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围在1~10之间,可以使用Thread类的三个静态常量:MAX_PRIORITY:其值为10,MIN_PRIORITY:其值为1,NORM_PRIORITY:其值为5。
线程同步
使用多个线程访问同一个数据时很容易出现此类状况,因此我们需要通过线程安全来进行处理。
线程安全问题——银行取钱的问题。银行取钱的基本流程上可以分为如下步骤:
1、用户输入账户、密码,系统判断用户的账户、密码是否正确。
2、用户输入取款金额
3、系统判断用户的余额是否大于取款金额
4、如果余额大于取款金额,则提取成功;如果余额小于取款金额则提取失败。
在单线程下没有任何问题,但这个流程一旦放入到多线程并发的情况下,则可能出现问题。可能是指有几率出现,也许我们将程序运行一百万次也没有出现,但是不代表没有问题。按照上述情况我们通过代码来验证。
Ø 同步代码块
为了解决此类问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized (obj) { // 此处的代码为同步代码 }
在上面语法格式中的obj就是同步监视器,其含义就是:线程在开始执行同步代码块之前,必须先获得对同步监视器的锁定。
Java程序允许使用任何对象作为同步监视器,但是我们试想一下同步监视器的目的是为了:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被多个线程并发访问的共享资源来充当同步监视器。因此对于上面取款模拟程序,应该考虑使用账户account对象作为同步监视器,把程序修改如下所示:
public class Test9 extends Thread { private Account account; private double money; /** * @param atmNo 取款的ATM编号 * @param account 要取款的账号 * @param money 取款的金额 * */ public Test9(String atmNo, Account account, double money){ super(atmNo); this.account = account; this.money = money; } @Override 在run方法里面用同步代码看将逻辑包裹起来,作用就是一个线程执行完之前 public void run() { 另一个线程需要等待 synchronized (account) { if(this.account.getAccount()>=money){ System.out.println(this.getName()+"号ATM取钱成功!吐出钞票:"+this.money); // 为了演示效果,故意在此停止程序执行表示网络之间的延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //修改余额 this.account.setAccount(account.getAccount()-money); System.out.println("余额为:"+account.getAccount()); }else{ System.out.println("余额不足!"); } } } public static void main(String[] args) { Account acount = new Account("1234567",1000); new Test9("0112",acount,800).start(); new Test9("0315",acount,800).start(); } }
修改后的程序使用synchronize将run()方法里面的方法体修改成同步代码块,该同步代码块的同步监视器就是account对象,这样的一个线程在执行run方法之后,执行完之后才会将同步监听器解锁释放,下一个线程才会执行run方法。这样就不会造成错误了。
任何线程在修改指定资源前,首先要获得改资源并对该资源进行加锁操作,在加锁期间其他线程无法修改该资源。当线程修改完成后,释放对资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进行修改共享资源的代码区(也被称为临界区),所以同一时刻最多有一个线程处于临界区内,从而保证了线程的安全性。
同步方法
与同步代码块相比,Java的多线程安全操作,还提供了同步方法。同步方法就是使用synchronized关键字来修饰某个方法,则此方法被称为同步方法。对于synchornized修饰的对象方法而言,无须显示的指定它同步监视器,同步方法的同步监视器就是当前this指向的对象,也就是调用该方法的对象。
对于前面讲过的取款模拟程序,如果更改成同步方法的形式。就需要将同步方法定义在Account对象中,因为线程在进行并发执行时,Account对象就是共享资源,因此就需要锁定Account对象。下面将Account对象的访问设置为线程安全的,那么只要把取款操作定义在Account类中即可。
public class Test9 extends Thread { private Account account; private double money; /** * @param atmNo 取款的ATM编号 * @param account 要取款的账号 * @param money 取款的金额 * */ public Test9(String atmNo, Account account, double money){ super(atmNo); this.account = account; this.money = money; } @Override public void run() { /*之前是将同步代码块写在这里,
因为我们将同步方法定义在了同步监听器的类中了, 所以直接调用那个方法即可*/ this.account.drawMoney(money); } public static void main(String[] args) { Account acount = new Account("1234567",1000); new Test9("0112",acount,800).start(); new Test9("0315",acount,800).start(); } }
同步监听器所在的类
public class Account { private String userNo; private double account; public Account(String userNo,double account){ this.userNo = userNo; this.account = account; } /*将同步方法定义在共享的资源中(可能出现错误的),*/ public synchronized void drawMoney(double money) { if(this.account>=money) { System.out.println(Thread.currentThread().getName()+"号ATM取钱成功!吐出钞票:"+money); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } //修改余额 this.setAccount(this.account-money); System.out.println("余额为:"+this.account); }else { System.out.println(Thread.currentThread().getName()+"取款失败,余额不足!"); } } public String getUserNo() { return userNo; } public void setUserNo(String userNo) { this.userNo = userNo; } public double getAccount() { return account; } public void setAccount(double account) { this.account = account; } }
当多个线程并发修改同一个Account对象时,必须先对此对象加锁,同样符合了 加锁——修改——释放锁的逻辑。
线程安全类是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序采用如下策略:1、不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源的方法进行同步,例如Accout对象中的userNo属性就无须同步
synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。
释放同步监视器的锁定
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么线程在何时会释放对同步监视器的锁定呢?程序无法通过调用方法的方式来显示的释放对同步监视器的锁定,线程会在以下几种情况来释放对同步监视器的锁定。
1. 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
2. 当前线程的同步代码块、同步方法中遇到break、return终止该代码块、该方法的继续执行,当前线程将会释放同步监视器。
3. 当前线程在同步代码块、同步方法中出现了未处理的Error或者Exception,导致了该代码块、该方法异常结束时,当期线程将会释放同步监视器。
4. 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
如果出现以下情况下,线程不会释放同步监视器。
1. 线程执行同步代码块或同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。
演示没有释放同步监视器的情况
package com.langsin.test; /* * 不释放同步监听器的情况 * */ public class ReadBook { public synchronized void readContent(){ for (int i = 0; i < 5; i++) { System.out.println("当前读者:"+Thread.currentThread().getName()+"正在读这本书"); if (i % 2 == 0){ System.out.println("读累了,休息一会"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
package com.langsin.test; public class Reader extends Thread{ private ReadBook readBook = null; public Reader(String username,ReadBook readBook){ super(username); this.readBook = readBook; } @Override public void run() { this.readBook.readContent(); } 没有释放同步监视器的情况下,会输出张三读书,累了休息,张三读书累了休息。。。
张三读完,才会轮到李四来,也就是下面定义的子线程,一个执行完,下一个子线程才能开始执行
这两个类的过程就是,Reader继承了Thread类,成为了线程类,在一个main方法中,定义
两个子线程,因为Reader是继承了Thread类的方式来创建的,所以直接new一个Reader的
实例对象,就相当于创建了线程对象,可以直接statr启动运行线程 public static void main(String[] args) { ReadBook readBook = new ReadBook(); new Reader("张三",readBook).start(); new Reader("李四",readBook).start(); new Reader("王五",readBook).start(); } }
当前读者:张三,正在读这本书!
当前读者:张三,正在读这本书!
读累了,休息一会.................
当前读者:张三,正在读这本书!
当前读者:张三,正在读这本书!
读累了,休息一会.................
当前读者:张三,正在读这本书!
当前读者:李四,正在读这本书!
当前读者:李四,正在读这本书!
读累了,休息一会.................
当前读者:李四,正在读这本书!
当前读者:李四,正在读这本书!
读累了,休息一会.................
当前读者:李四,正在读这本书!
从执行结果可以看成,当其中一个线程抢占到共享资源时,在线程任务没有结束的情况下,并不会释放该资源。
同步锁:Lock
同步锁Lock。通过显示定义同步锁对象来实现同步,在这种机制下,同步监视器使用Lock对象来充当。
在实现线程安全的控制中,常用的是ReentrantLock(Lock的实现类),使用该Lock可以显示的加锁、释放锁。
使用ReentrantLock进行加锁的代码格式如下:
package com.langsin.test; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private ReentrantLock lock = new ReentrantLock(); public void method(){ lock.lock();//加锁 try { //保证代码的安全机制,如果出现异常,确保锁对象会被释放 }finally { lock.unlock();//释放锁 } } }
使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,
通常建议使用finally块来确保在必要时释放锁。
通过使用ReentrantLock对象,可以把Account类改为如下形式,它依然是线程安全的。
package com.langsin.test; import java.util.concurrent.locks.ReentrantLock; public class Account { private String userNo; private volatile double account; private ReentrantLock lock = new ReentrantLock(); private Account(String userNo,double account){ this.userNo = userNo; this.account = account; } public void drawMoney(double money){ lock.lock(); try { /*业务逻辑,这个方法是要在run方法中调用的*/ }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } }
ReentrantLock锁具有可重入性,也就是说一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁对象保护的方法。
package com.langsin.test; import java.util.concurrent.locks.ReentrantLock; public class ReentrantDemo { private ReentrantLock lock = new ReentrantLock(); public void method1(){ lock.lock(); System.out.println("方法1进入同步执行"); this.method2(); lock.unlock(); } public void method2(){ lock.lock(); System.out.println("方法2进入同步执行"); this.method1(); lock.unlock(); } }
死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁,死锁无法解决,只能预防。
一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续执行。
例如:房东没带钥匙,想要开锁公司打开房门,但是开锁公司打开房门之前需要先看房产证上的名字,而房产证又放在屋里。
package com.langsin.test; public class House { public synchronized void open(HouseLicense license){ System.out.println("开锁公司可以打开房门,但是需要看房产证上的房东的名字!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } license.showHouseOwner(); } public synchronized void inHouse(){ System.out.println("房东想进房子"); } }
房产证明
package com.langsin.test; /* * 房产证明 * */ public class HouseLicense { public synchronized void showHouseOwner(){ System.out.println("能够证明房主是谁"); } public synchronized void getHouseLicense(House house){ System.out.println("房东想要拿房产证,但是需要先进入房子!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } house.inHouse(); } }
package com.langsin.test; /* * 房东线程类HouseOwner * */ public class HouseOwner extends Thread{ private House house = null; private HouseLicense license = null; public HouseOwner(House house,HouseLicense license){ this.house = house; this.license = license; } @Override public void run() { this.license.getHouseLicense(house); } }
package com.langsin.test; public class Opener extends Thread{ private House house = null; private HouseLicense license = null; public Opener(House house,HouseLicense license) { this.house = house; this.license = license; } @Override public void run() { this.house.open(license); } }
测试
package com.langsin.test; public class DeathTest { public static void main(String[] args) { House house = new House(); HouseLicense license = new HouseLicense(); new HouseOwner(house, license).start(); new Opener(house, license).start(); } }
程序没有终止并且执行不下去,出现了等待的现象。究其原因为:House、HouseLicense对象里的方法都是同步方法,执行前都需要先对对象加锁。
房东线程,调用房产证的getHoseLicense方法,因此对license对象加锁,休眠,切换到开锁线程
开锁线程,调用房子的open方法,因此对house对象加锁,休眠,切换到房东线程
房东线程,需要调用house对象的inHouse方法,需要对house对象加锁,而此时开锁线程对house对象的锁没有释放,阻塞
开锁线程,需要调用license对象的showHouseOwner方法,需要对license对象加锁,而此时房东线程对license对象的锁没有释放,阻塞
因此出现死锁现象。
在Thread类中的suspend()就非常容易导致死锁情况的出现,因此Java不再推荐使用该方法,此方法已被废弃。
死锁的四个必要条件
1. 互斥条件:线程对所分配到的资源不允许其他线程进行访问,若其他线程访问该资源,只能等待,直至占有该资源的线程使用完成后释放该资源
2. 请求和保持条件:线程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他线程占有,此时请求阻塞,但又对自己获得的资源保持不放
3. 不可剥夺条件:是指线程对于已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
4. 环路等待条件:是指线程发生死锁后,必然存在一个线程--资源之间的环形链。
线程通讯
程序无法准确的控制线程的切换执行,通过一些机制来协调线程之间的运行成为通信。
传统的线程通信
业务场景:
一个人存钱,一个人取钱,只能先存钱接着取钱,不能连续存钱或取钱。
通过由同步监视器来调用wait()、notify()和notifyAll()3个方法,实现上述业务。
同步监视器分为两种情况:
1. 使用synchronized修饰的同步方法,同步监视器就是当前实例对象,所以此方法由this直接调用
2. 使用synchronized修饰的同步代码块,同步监视器是synchronzied后括号里的对象,所以必须使用该对象调用这3个方法。
对此3个方法的解释如下:
-
wait():导致当前线程进入等待,直到其他线程调用该同步监视器的notify()方法或者notifyAll()方法来唤醒该线程。该wait()方法有三种形式——无时间参数的wait,一直等待,直到其他线程的唤醒;带毫秒参数的wait和带毫秒、微秒参数的wait,这两种方法都是等待指定时间后自动苏醒,调用wait方法的当前线程会释放对该同步监视器的锁定。
-
notify():唤醒此同步监视器上等待的单个线程。如果所有的线程都在此同步监视器上等待着,则会唤醒其中一个线程。选择是任意的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
-
notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
package com.langsin.test; /* * BankAccount类,定义线程同步的方法,在子线程中的run方法中调用 * */ public class BankAccount { // flag为false表示账户中没有钱 private boolean flag = false; // 取款操作的方法 public synchronized void drawMoney() throws Exception { if (this.flag){ System.out.println("进行取款操作,取走金额为800,当前账户余额为0!"); this.flag = false; Thread.sleep(1000); this.notifyAll(); }else { this.wait(); //this表示同步监视器,也就是BankAccount } } // 存款操作的方法 public synchronized void dispoteMoney() throws Exception{ if (this.flag) { this.wait(); } else { System.out.println("进行存款操作,存款金额为800,当前账户余额为800!"); this.flag = true; Thread.sleep(1000); this.notifyAll(); } } }
存款线程
package com.langsin.test; /* * 取款子线程 * */ public class DrawThread extends Thread{ private BankAccount bankAccount = null; public DrawThread(BankAccount bankAccount){ this.bankAccount = bankAccount; } @Override public void run() { try { while (true) { this.bankAccount.drawMoney(); } } catch (Exception e) { e.printStackTrace(); } } }
取款线程
package com.langsin.test; /* * 存款子线程 * */ public class DispoteThread extends Thread{ private BankAccount bankAccount = null; public DispoteThread(BankAccount bankAccount) { this.bankAccount = bankAccount; } @Override public void run() { try { while (true) { this.bankAccount.dispoteMoney(); } }catch (Exception e){ e.printStackTrace(); } } }
测试
package com.langsin.test; public class Test1 { public static void main(String[] args) { BankAccount bankAccount = new BankAccount(); new DispoteThread(bankAccount).start(); new DrawThread(bankAccount).start(); } }
使用Condition控制线程通信
如果不使用synchronized关键字来保证同步,而是使用Lock对象来保证同步,这样系统中就没有同步监视器,也就无法通过同步监视器来调用wait()、notify()、notifyAll()方法进行线程通信了。
所以Lock替代了同步方法或者同步代码块,来实现加锁解锁功能,而Condition替代了同步监视器的功能。 比如下面代码
package com.langsin.test; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /* * BankAccount类,定义线程同步的方法,在子线程中的run方法中调用 * */ public class BankAccount { // flag为false表示账户中没有钱 private boolean flag = false; /*创建lock对象*/ private ReentrantLock lock = new ReentrantLock(); /*创建Condition对象*/ private Condition condition = lock.newCondition(); // 取款操作的方法 通过lock.lock的方式手动加锁,所以就不需要synchronized关键字了 public void drawMoney() throws Exception { lock.lock(); if (this.flag){ System.out.println("进行取款操作,取走金额为800,当前账户余额为0!"); this.flag = false; Thread.sleep(1000); this.notifyAll(); }else { condition.await(); 代替同步监视器来调用等待方法 } lock.unlock(); //因为去掉了synchronized关键字,所以同步监视器就不是当前实例了,不能用this.unlock了 } // 存款操作的方法 public void dispoteMoney() throws Exception{ lock.lock(); if (this.flag) { condition.await(); } else { System.out.println("进行存款操作,存款金额为800,当前账户余额为800!"); this.flag = true; Thread.sleep(1000); this.notifyAll(); } lock.unlock(); } }
使用阻塞队列控制线程通信
BlockingQueue也是Queue的子接口,作为线程同步的工具。
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
BlockingQueue提供如下两个支持阻塞的方法:
² put(Element e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则放弃更改队列,阻塞线程。
² take():尝试从BlockingQueue的头部取出该元素,如果该队列的元素已空,则阻塞该线程。
1. 在队列尾部插入元素。包括add、offer、put方法,当队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
2. 在队列头部删除并返回该删除的元素。包括remove、poll、take方法,当队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
3. 在队列头部取出但不删除元素,包括element、peek方法,当队列已空时,这两个方法分别抛出异常、返回false。
BlockingQueue包含如下5个实现类:
1. ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
2. LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
3. PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似,该队列调用了remove、poll、take方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。
4. SynchronousQueue:同步队里,该队列的存、取操作必须交替进行。
5. DelayQueue:它是一个特殊BlockingQueue,底层基于PriorityBlockingQueue实现。不过DelayQueue要求集合元素都实现Delay接口,DelayQueue根据集合元素的getDelay返回值进行排序。
生产者代码
package com.langsin.test; import java.util.concurrent.ArrayBlockingQueue; /* * 生产者线程 * */ public class Producer extends Thread{ /*阻塞队列*/ private ArrayBlockingQueue<String> queue = null; public Producer(ArrayBlockingQueue<String> queue){ this.queue = queue; } @Override public void run() { for(int i=0;i<3;i++) { try { String value = "产品"+i; this.queue.put(value); System.out.println("生产者放入产品:"+value); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }
消费线程
package com.langsin.test; import java.util.concurrent.ArrayBlockingQueue; /* * 消费者线程 * */ public class Customer extends Thread{ private ArrayBlockingQueue<String> queue = null; public Customer(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { for(int i=0;i<3;i++) { try { //使用了继承Thread的方式,那this就代表当前这个类的实例,也就是线程对象 String value = this.queue.take(); System.out.println("消费者取走产品:"+value); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } } }
测试
package com.langsin.test; import java.util.concurrent.ArrayBlockingQueue; public class Test3 { public static void main(String[] args) { ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); new Producer(queue).start(); new Customer(queue).start(); } /* * 消费者取走产品:产品0 生产者放入产品:产品0 生产者放入产品:产品1 消费者取走产品:产品1 生产者放入产品:产品2 消费者取走产品:产品2 * */ }
生成者放入一个产品后,消费者取走一个产品。如果下次执行时,消费者线程抢占到执行机会,但是集合中没有对象,因此进入阻塞状态。等生成者放入产品后,消费者线程立刻将产品取走。如果生成者线程抢占到执行机会,但是此时集合中有产品,生成者线程也会进入阻塞状态。
阻塞状态是由集合的方法put与take进行阻塞的。
线程池
与数据库连接池比较相似的是,线程池在系统启动时即创建大量空闲的线程
程序将一个Runable对象或者Callable对象传递给线程池,线程池就会启动一个线程来执行它们的run方法或者call方法。
当run方法或者call方法执行结束后,线程并不会死亡,而是再次返回线程池成为一个空闲线程,等待执行下一个Runable的run方法或者Callable对象的call方法。
Executors工厂类来生成线程池,该工厂类提供了如下几个静态方法来创建线程池
-
1. newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存到线程池中。
-
1. newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
-
1. newSingleThreadExecutor():创建一个只有单线程的线程池,相当于调用了newFixedThreadPool方法时,只传入了1.
-
1. newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务,corePoolSize是指线程池中的线程数,即使线程是空闲的也会被保存线程池中。
- newSingleThreadScheduleExecutor():创建只有一个线程的线程池,它可以在指定的延迟后执行线程任务
在上面5个方法中的前3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runable对象或者Callable对象所代表的线程;而后2个方法返回一个ScheduleExecutorService线程池,它是ExecutorService子类,它可以在指定延迟后执行线程任务
ExecutorService代表尽快执行线程的线程池,程序只要将一个Runable对象或者Callable对象提交给线程池,该线程池就会尽快的执行该任务。ExecutorService提供如下3个方法:
- Future submit(Runable target):将一个Runable对象提交给线程池,线程池将会在有空闲线程的时候执行该Runable对象的代表的任务。
² Future代表执行Runable任务的返回值,但是Runable的run方法是没有返回值的,所以Future对象在run方法执行完成后返回null,但是可以调用Future的isDone、isCancelled的方法来获得Runable对象的执行状态。
-
² <T> Future<T> submit(Runable task,T result):将一个Runable对象提交给指定的线程池,线程池将在有空闲线程时执行Runable对象代表的任务。其中result为显示的指定线程执行结束后的返回值,所以Future对象将在run方法执行结束后返回result。
-
² <T> Future<T> submit(Callable<T> task):将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里的call方法的返回值。
ScheduleExecutorService代表可在指定的延迟后或者周期性的执行线程任务的线程池,它提供了如下4个方法:
-
² ScheduledFuture<T> schedule(Callable<T> call,long delay,TimeUnit unit):指定callable任务将在delay延迟后执行,unit为时间单位。
-
² ScheduledFuture<T> schedule(Runable<T> command,long delay,TimeUnit unit):指定command任务将在delay延迟后执行,unit为时间单位。
-
² ScheduledFuture<T> scheduleAtFixedRate(Runable command,long delay,long period,TimeUnit unit):指定command任务将在delay延迟后执行,而且以设定的频率重复执行。也就是说在delay后开始执行,并且在delay+period、delay+2*period…处进行重复执行,依次类推。
-
² ScheduledFuture<T> scheduleWithFixedDelay(Runable command,long delay,long period,TimeUnit unit):创建并执行一个在给定初始延迟后首次启用任务的定期操作,随后在每一次终止和下次开始之间都存在给定的延迟。如果任务在任意一次执行时遇到异常,就会取消后续的操作;否则只能通过程序来进行显示的取消或终止该任务。

浙公网安备 33010602011771号