文章目录
参考书目
《Java多线程编程核心技术》
一、Java多线程技能
1.1 进程和多线程的概念及线程的优点
进程:进程是受操作系统管理的基本运行单元。
线程:线程可以理解成是在进程中独立运行的子任务。
多线程优点:可以最大限度地利用CPU的空闲时间(比如一个下载任务线程需要等待远程数据传输完毕)来处理其他的任务, 提升系统的运行效率,使用多线程也就是在使用异步。
1.2 使用多线程
1.2.1 继承Thread类
实现多线程编程的方式主要有两种,一种是继承Thread类(缺点:无法多继承),另一种是实现Runnable接口。
public class Thread implements Runnable
如上,Thread类实现了 Runnable接口,它们之间具有多态关系。
//线程实现类
public class Test extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
//主函数调用线程
public static void main(String[] args) {
Test myThread = new Test();
myThread.start();
System.out.println("运行结束!");
}
}
执行上述代码时两个输出语句顺序随机,说明在使用多线程时,代码的运行结果与代码执行顺序或调用顺序是无关的。
线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法,所以就会出现先打印“运行结束!”后输出"MyThread“这样的结果了。
可见线程具有随机特性:
Thread.java类中的 start() 方法通知“线程规划器”此线程已经准备
就绪,等待调用线程对象的run()方法。这个过程让系统安排
一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动
线程,具有异步执行的效果。注意start()方法的执行顺序不代表线程启动顺序。
如果调用代码 thread.run() 就不是异步执行
,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代
码执行完后才可以执行后面的代码。
1.2.2 实现Runnable接口
//线程实现类
public class Test implements Runnable {
@Override
public void run() {
System.out.println("运行中");
}
//主函数调用线程
public static void main(String[] args) {
Runnable runnable = new Test();
Thread myThread = new Thread(runnable);
myThread.start();
System.out.println("运行结束!");
}
}
实现Runnable接口的方法支持多继承。
需要说明的是,Thread.java类也实现了 Runnable接口,因此构造函数 Thread(Runnable target) 不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他由线程进行调用。
1.2.3 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多线程交互时很重要。
- 不共享数据
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);// 设置线程名称
}
@Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由 " + Thread.currentThread().getName() + "计算,count=" + count);
}
}
}
以上线程定义多个实例并运行时可以看到各自的count值在分别减少,说明变量不共享。
- 共享数据
public class MyThread extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
//此示例不要用for语句,因为使用同步后其他线程就得不到运行的机会了,
// 一直由一个线程进行减法运算
count--;
System.out.println("由 " + Thread.currentThread().getName() + "计算,count=" + count);
}
}
以上线程多个实例调用时运行结果如图:
注意到A和B打印结果相同
在某些JVM中,i–的操作要分成如下3步:
1)取得原有i值。
2)计算i-1。
3)对i进行赋值。
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
解决方法:对需要保证同步执行的方法加上 synchronized 关键字,比如上文代码的run()方法:
synchronized public void run()
执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。
synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为 “互斥区”或“临界区” 。
多个线程会同时争抢同步锁。
1.2.4 留意i- -与System.out.println()的异常
public class MyThread extends Thread {
private int i = 5;
@Override
public void run() {
System.out.println("i-" + (i--) + " threadName-"
+ Thread.currentThread().getName());
"
//
注意:代码i--由前面项目中単独一行运行改为在当前项目中在println()方法中直接进行打印
}
}
虽然println()方法在内部是同步的,但的操作i–却是在进入println()之前发生的,所以有发生非线程安全问题的概率。
1.3 常用API
- currentThread()方法
返回代码段正在被哪个线程调用。
Thread.currentThread().getName();
this.getName();
这里需要强调 Thread.currentThread() 和 this 的差异:
在自定义线程类时,如果线程类是继承java.lang.Thread的话,那么线程类就可以使用this关键字去调用继承自父类Thread的方法,this就是当前的对象。
另一方面,Thread.currentThread()可以获取当前线程的引用,一般都是在没有线程对象又需要获得线程信息时通过Thread.currentThread()获取当前代码段所在线程的引用。
public class MyThread extends Thread {
public MyThread(){
System.out.println("------" + "构造函数开始" + "------");
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive() = " + Thread.currentThread().isAlive());
System.out.println("this.getName() = " + this.getName());
System.out.println("this.isAlive() = " + this.isAlive());
System.out.println("------" + "构造函数结束" + "------");
}
@Override
public void run(){
System.out.println("------" + "run()开始" + "------");
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive() = " + Thread.currentThread().isAlive());
System.out.println("this.getName() = " + this.getName());
System.out.println("this.isAlive() = " + this.isAlive());
System.out.println("Thread.currentThread() == this : " + (Thread.currentThread() == this));
System.out.println("------" + "run()结束" + "------");
}
}
测试类:
public class Test {
public static void main(String[] args){
MyThread myThread = new MyThread();
myThread.setName("A");
myThread.start();
}
}
测试结果(’#'后为分析):
------构造函数开始------
Thread.currentThread().getName() = main #实例化MyThread,调用MyThread构造方法是主线程main
Thread.currentThread().isAlive() = true
this.getName() = Thread-0 #this是MyThread的引用,是个线程类
this.isAlive() = false #由于这个线程类并没有设置名字,所以Thread默认命名Thread-0
------构造函数结束------
------run()开始------
Thread.currentThread().getName() = A #当前线程名字为A,A是我们手动赋予的myThread.setName("A");
Thread.currentThread().isAlive() = true #说明它是运行着的
this.getName() = A #运行的线程就是MyThread的引用
this.isAlive() = true #而this也是MyThread的引用,所以打印结果与Thread.currentThread()相同
Thread.currentThread() == this : true
------run()结束------
修改测试类如下:
public class Test {
public static void main(String[] args){
MyThread myThread = new MyThread();
// 将线程对象以构造参数的方式传递给Thread对象进行start()启动线程
Thread newThread = new Thread(myThread);
newThread.setName("A");
newThread.start();
}
}
修改后的测试结果如下:
------构造函数开始------
Thread.currentThread().getName() = main
Thread.currentThread().isAlive() = true
this.getName() = Thread-0
this.isAlive() = false
------构造函数结束------
------run()开始------
Thread.currentThread().getName() = A #Thread.currentThread()是Thread的引用newThread
Thread.currentThread().isAlive() = true
this.getName() = Thread-0 #this依旧是MyThread的引用
this.isAlive() = false
Thread.currentThread() == this : false
------run()结束------
直接启动的线程实际是newThread,而作为构造参数的myThread,赋给Thread类中的属性target,之后在Thread的run方法中调用target.run()。
- isAlive()方法
判断当前线程是否处于活动状态。
活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态。
mythread.isAlive();
- sleep()方法
让当前线程休眠(暂停执行),当前线程指Thread.currentThread()返回的线程。
Thread.sleep(2000);
- getId()方法
获取线程的唯一标识。
mythread.getId();
- yield()方法
放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但是放弃时间不确定,有可能刚刚放弃,马上有获得CPU时间片。
Thread.yield();
1.4 停止线程
有三种方法可以结束线程:
- 设置退出标志,使线程正常退出,也就是当run()方法完成后线程终止
- 使用interrupt()方法中断线程
- 使用stop方法强行终止线程(不推荐使用,Thread.stop,Thread.suspend,Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
1.4.1 判断线程是否是停止状态
1) this.interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志置清除为false的功能。
2) this.islnterrupted():测试线程Thread对象是否已经是中断状态,但不清除状态标志。
1.4.2 interrupt()方法中断线程
interrupt():给当前线程打上停止标记
1)线程处于阻塞状态
如使用了sleep,同步锁的wait,socket中的receive、accept等方法时,会使线程处于阻塞状态,此时调用线程的interrupt()方法,会抛出InterruptException异常。
阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。
很容易误认为只要调用interrupt方法线程就会结束,实际上一定要先捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
public class ThreadSafe extends Thread {
public void run() {
while (true){
try{
Thread.sleep(5*1000);//阻塞5s
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行break跳出循环。
}
}
}
public static void main(String[] args){
ThreadSafe myThread = new ThreadSafe();
myThread.start();
myThread.interrupt();
}
}
2)线程处于非阻塞状态
使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){
//do something, but no throw InterruptedException
}
//如果循环后还有可执行代码,上面的循环跳出后会继续执行
}
}
抛异常方式结束线程:
public class ThreadSafe extends Thread {
public void run() {
while (true){
//do something
if(isInterrupted()){
throw new InterruptedException();
}
}
}
}
为什么要区分进入阻塞状态和和非阻塞状态两种情况
当阻塞状态时,如果有interrupt()发生,系统除了会抛出InterruptedException异常外,还会自动调用interrupted()函数,调用时能获取到中断状态是true的状态,调用完之后会复位中断状态为false,所以异常抛出之后再通过isInterrupted()是获取不到正确的中断状态的,从而不能退出循环,因此在线程未进入阻塞的代码段时是可以通过isInterrupted()来判断中断是否发生来控制循环,在进入阻塞状态后要通过捕获异常来退出循环。
推荐的停止线程的方法
将两种情况的处理结合起来
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行break跳出循环。
}
}
}
}
1.4.3 强行停止线程
程序中可以直接使用 thread.stop() 来强行终止线程,但是stop方法是很危险的,会强行终止可能正在运行的线程,创建子线程的线程会抛出java.lang.ThreadDeath异常,并且会释放子线程所持有的所有锁。
不安全主要在于:thread.stop()调用之后,可能会导致一些清理工作得不到完成,也可能会导致锁定的对象进行了解锁,出现数据不一致的问题。
1.4.4 return停止线程
和1.4.2的非阻塞状态下的非抛异常的方法同理,都是使程序通过检测中断标记控制线程正常结束。
public class ThreadSafe extends Thread {
public void run() {
while (true){
//do something
if(this.isInterrupted()){
return;
}
}
}
}
1.5 暂停线程
MyThread thread = new MyThread();
thread.start();
Thread.sleep(5000);
thread.suspend();
//暂停线程
Thread.sleep(5000);
thread.resume();
//恢复线程
suspend与resume方法缺点
在使用suspend与resume方法时,如果使用不当,极易造成公共的同步对象的独占,使
得其他线程无法访问公共同步对象。
在使用suspend与resume方法时,也容易出现因为线程的暂停而导致数据不同步的情况。
1.6 线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
在java中,线程分了1-10个等级,默认为5。
myThread.setPriority(6);
myThread.getPriority();
线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
CPU会把资源让给优先级比较高的线程。
优先级也具有随机性,优先级高的不一定每一次都先执行完(看谁运行的快,平均情况下优先级较高的线程运行较快)。
1.7 守护线程
线程分两种,一种是用户线程,一种是守护线程。
守护线程守护非守护线程,当不存在非守护线程了,守护线程自动销毁。
典型的守护线程如垃圾回收线程。
Thread myThread = new Thread();
myThread.setDaemon(true);
二、对象及变量的并发访问
非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是脏读,也就是取到的数据其实是被更改过的。
而“线程安全”就是以获得的实例变量的值是经过同步处理,不会出现脏读的现象。
线程安全问题产生的原因:1.多个线程在操作共享的数据。2.操作共享数据的线程代码有多条。当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。
解决思路就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕之后,其他线程才可以参与运算。
2.1 synchronized关键字
synchronized作用
synchronized可以用于线程间的互斥,还可以确保变量在内存的可见性(关于其保证可见性看2.2节)
2.1.1 多个对象多个锁
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方 法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个 对象,即Java对象的内存地址相同。
//同步代码块
synchronized(object){
//需要被同步的代码
}
//同步方法,同步方法是synchronized关键字的隐式调用,使用的锁是this
synchronized public void methodName{
//需要被同步的代码
}
A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
//测试实体类
public class PublicVar {
public String username = "NAME";
public String password = "PASSWORD";
synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println(Thread.currentThread().getName() + ": username="
+ username + " password=" + password);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//getValue()是非synchronized类型的方法,在多个线程操作同一个对象实例时如果都调用了getValue不能保证该方法同步,就会出现脏读现象
public void getValue() {
System.out.println(Thread.currentThread().getName() + ": username="
+ username + " password=" + password);
}
}
2.1.2 可重入锁
自己可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
可重入锁也支持在父子类继承的环境中,子类可以通过
“可重入锁”调用父类的同步方法的。
但需要注意同步不具有继承性,子类在重写父类的同步方法时如果也需要同步效果需要加上synchronized。
2.1.3 同步方法与同步代码块
同步方法使用的锁是this,同步方法的弊端是同步方法内代码不需要同步的部分也对this保持了同步,并且同步方法有多个时线程会受到阻塞,等待时间变长,运行效率降低。
同步代码块使用 synchronized(this) 时和同步方法效果相同,都是对this所指的当前对象保持同步,但是可以灵活选择需要保持同步的部分,从而提高运行效率。
synchronized 同步方法或 synchronized(this)同步代码块的作用:
1) 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
2) 同一时间只有一个线程可以执行synchronized同步方法或 synchronized(this)同步代码块中的代码。
synchronized(非this对象 x) 同步代码块:
在多个线程持有“对象监视器”为同一个对象的前提下,对其他 synchronized(x){} 调用呈阻塞状态,并且同一时间只有一个线程可
以执行 synchronized(非this对象 x) 同步代码块中的代码。
优点是:synchronized(非this对象 x) 同步代码块和同步方法是异步的,不会争抢this锁
//使用同步方法
public class Service {
synchronized public void methodA () {
System.out.println("methodA begin");
while (true) {
//do something
}
System.out.println("methodA end");
}
synchronized public void methodB () {
System.out.println("methodB begin");
System.out.println("methodB end");
}
}
//测试线程
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service){
super();
this.service = service;
}
@Override
public void run() {
service.methodA();
}
}
线程A调用该同步方法后除非中断线程,否则其他线程永远无法获得this锁,其他同步方法的调用也会阻塞,可以使用同步代码块(非this对象)解决。
//使用同步代码块
public class Service {
public Object lock1 = new object();
public Object lock2 = new Object();
public void methodA () {
synchronized (lock1){
System.out.println("methodA begin");
while (true) {
//do something
}
System.out.println("methodA end");
}
}
public void methodB () {
synchronized (lock2){
System.out.println("methodB begin");
System.out.println("methodB end");
}
}
}
2.1.4 静态同步synchronized方法和同步 synchronized(class) 代码块
静态方法的锁不是this,经过验证可以知道静态函数的锁是字节码文件对象。静态的同步函数使用的锁是该函数所属的字节码文件对象 可以用getClass方法获取(这里要注意getClass方法是非静态的,使用要谨慎),也可以用当前类名.class(class是静态属性)形式表示。
2.1.5 synchronized(String s)时要注意常量池带来的问题
public class Test {
public static void main(String[] args) {
String a = "a";
String b = "a";
System.out.println (a == b);
//打印结果为true, 常量池特性,如果用a和b分别作为锁对象,则判断时为认为这两个锁是相同的
}
}
2.1.6 死锁问题
Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”。
public class DealThread implements Runnable {
public String username;
public Object lock1 = new object();
public Object lock2 = new Object();
public void setFlag(String username) {
this.username = username;
}
//实现死锁,以下代码使用嵌套结构实现,还有其他方法。只要互相等待对方释放锁就有可能出现死锁。
@Override
public void run(){
if (username.equals("a")) {
synchronized (lock1) {
try {
System.out.println("username = " + username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lock2) {
System.out.println ("按 lockl->lock2 代码顺序执行了 ");
}
}
}
if(username.equals("b")) {
synchronized (lock2) {
try {
System.out.println("username = " + username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("按 lock2->lock1 代码顺序执行了 ");
}
}
}
}
}
public class Run {
//测试主函数
public static void main(String[] args){
try {
Test t1 = new Test();
t1.setFlag("a");
Thread thread1 = new Thread(t1);
thread1.start();
Thread.sleep(100);
t1.setFlag("b");
Thread thread2 = new Thread(t1);
thread2.start();
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
可以使用JDK自带的工具来监测是否有死锁的现象。首先进入CMD工具,再进入JDK
的安装文件夹中的bin目录,执行jps命令。
得到运行的线程Run的id是3244,再执行jstack命令
结果如下,检测到死锁:
2.1.7 内置类与静态内置类
在内置类中有多个同步方法,如果使用的是不同的锁,打印的结果 也是异步的。
public OuterClass{
static class InnerClass1{
public void method1(InnerClass2 class2){
synchronized (class2){
//do something
}
}
}
static class InnerClass2{
synchronized public void method1(){
//do something
}
}
}
上述代码中,同步代码块 synchronized(class2) 对class2上锁后,其他线程只能以同步的方 式调用class2中的静态同步方法,也就是说 一个线程调用了InnerClass1中的method1会对class2上锁,其他线程想调用InnerClass2中的静态同步方法则需要等待第一个线程解锁。
2.1.8 锁对象的改变
synchronized(obj)语句块中,若obj对象发生改变,则其他线程可以进入synchronized语句块。
2.2 volatile关键字
关键字volatile的主要作用是使变量在多个线程间可见。
volatile要求线程每次都要从共享内存(公共堆栈)中读写变量,而不是在线程的工作内存中,因此保证了变量的可见性,但并不保证原子性,因此一般只能用于保证读取的是最新的数据。
使用volatile后的线程读写流程:
2.2.1 volatile与死循环
public class RunThread extends Thread {
private boolean isRunning = true;
//volatile private boolean isRunning = true; 使用关键字后的效果对比
public boolean getRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println ("进入run了");
while (isRunning == true) { //-server模式下jvm为了保证线程运行效率,是直接从私有堆栈中取到的isRunning值
System.out.println ("线程被停止了!");
}
}
//测试主函数
public static void main(String[] args){
try {
RunThread thread = new RunThread ();
thread.start();
Thread.sleep(1000);
thread.setRunning(false); //这里是把变量值保存到公共堆栈中
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
关键字synchronized和volatile比较
(1)关键字 volatile 是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 要好,并且 volatile 只能修饰于变量,而 synchronized 可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发使用 synchronized 关键字的比率还是比较大的。
(2)多线程访问 volatile 不会发生阻塞,而 synchronized 会出现阻塞。
(3)volatile能保证数据的可见性,但不能保证原子性(同步性);而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
(4)关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。
2.2.2 volatile非原子特性
表达式count++(变量 count 是volatile修饰的)其实并不是一个原子操作,也就是非线程安全的。
操作步骤分解如下:1)从内存中取出 count 的值;
2)计算 count 的值;
3)将 count 的值写到内存中。
1)read和load阶段:从主存复制变量到当前线程工作内存;
2)use和assign阶段:执行代码,改变共享变量值;
3)store和write阶段:用工作内存数据刷新主存对应变量的值。
在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后(volatile直接修改主存),线程工作内存中的值由于已经加 载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结 果会和预期不一样,也就出现了非线程安全问题。
2.2.3 使用原子类
原子操作是不能分割的整体,没有其他线程能够中断或检査正在原子操作中的变量。
一
个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全
(thread-safe)。
public class AddCountThread extends Thread {
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run(){
for (int i = 0; i < 10000; i++){
System.out.println(count.incrementAndGet());
}
}
}
要注意原子类也并不完全安全,原子类在具有逻辑性的情况下输出结果也具有随机性。
public class MyService{
public static AtomicLong aiRef = new AtomicLong();
//多线程调用该方法时会发现线程间addNum()的aiRef.addAndGet(100)和下一行的addAndGet(1)的执行顺序是随机的;
//说明多个原子方法的调用不是原子的,需要加上synchronized保证原子性
synchronized
public void addNum(){
System.out.println(Thread.currentThread().getName() + "加了 100之后的值是:"
+ aiRef.addAndGet(100));
aiRef.addAndGet(1);
}
}
三、线程间通信
3.1 等待/通知机制
3.1.1 相关方法
1)wait()方法
方法wait的作用是使当前执行代码的线程进行等待,wait方法是Object类的方法。该方法用来将当前线程置入“预执行队列”中,并且在wait所在的代码处停止执行,直到接到通知或被中断为止。
在调用wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中调用wait方法。在执行wait方法后,当前线程释放锁。在从wait返回前,线程与其他线程竞争重新获得锁。如果调用wait时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch语句进行捕获异常。
另有:带一个参数的 wait(long) 方法,功能是等待某一时间内是否有线程对锁进行唤醒,如果
超过这个时间则自动唤醒。
2)notify()方法
方法notify也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify时没有持有适当的锁,则抛出IllegalMonitorStateException。
该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。
需要说明的是,在执行notify方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify方法的线程将程序执行完,也就是退出synchronized代码块之后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。
另有:notifyAll()方法,功能是唤醒同一锁对象的所有wait中的线程。
wait/nodify 的使用如下:
//等待线程
public class MyThread1 extends Thread {
private Object lock;
public MyThread1(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("开始
等待:"+System.currentTimeMillis());
lock.wait();
System.out.println("结束等待:"+System.currentTimeMillis());
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//通知线程
public class MyThread2 extends Thread {
private Object lock;
public MyThread2(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println("开始通知:"+System.currentTimeMillis());
lock.notify();
System.out.println("结束通知:"+System.currentTimeMillis());
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
3)等待唤醒机制涉及的方法总结
- wait(): 让线程处于阻塞状态,这时线程会释放执行资格和执行权,被wait的线程会被存储到线程池中;
- notify(): 唤醒线程池中的任意一个线程;
- notifyAll(): 唤醒线程池中的所有线程,这些方法都必须定义在同步中,使对应锁等待中的所有线程进入可运行状态。
因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程。例如在objectA锁里wait必须在该锁里notify,也就是说,等待和唤醒必须是同一个锁。由于锁可以是任意对象,故wait/notify相关方法都定义在Object类中,也就是说这些方法通过对象调用。
4)notifyAll()方法—wait线程间条件值改变引发的问题
notifyAll执行后可能会出现多个wait线程间条件值改变引起的逻辑问题,可以用while判断等方法多次判断确保满足条件。
问题代码示例如下:
//用来存储值的对象
public class ValueObject {
public static List<Integer> list = new ArrayList<>();
}
//ThreadA的run部分代码
synchronized (lock) {
if (ValueObject.list.size () == 0) {
// 改为while (ValueObject.list.size () == 0),保证唤醒后可以再次判断条件
System.out.println ("wait begin ThreadName:"+ Thread.currentThread().getName());
lock.wait();
}
ValueObject.list.remove();
}
//ThreadB的run部分代码
synchronized (lock) {
ValueObject.list.add(0);
lock.nodifyAll();
}
如果ThreadA线程有两个实例同时运行并进入等待,然后ThreadB的一个线程实例给list添加了一个值并唤醒所有,则被唤醒的第一个线程可以正常remove,但被第二个唤醒的线程由于list.size()实际已经等于0所以remove会抛出异常;
出现这个问题的原因是 if(ValueObject.list.size () == 0) 的条件判断在wait前就已经执行了,wait返回后会直接跳出if语句而无法对条件再次判断以保证remove操作可以执行,解决的方法就是将if判断改为while。
3.1.2 线程生命周期
新建:创建新线程并调用start方法,线程进入runnable(可运行)状态,如果抢占到cpu资源,线程进入running(运行)状态。
就绪:线程进入 可运行状态(runnable) 有五种情况:
1.调用sleep方法超过指定休眠时间
2.线程调用的阻塞IO已经返回,阻塞方法执行完毕
3.线程获得了试图同步的监视器
4.线程正在等待通知,其他线程发出通知
5.处于挂起状态的线程调用了resume恢复方法
阻塞:线程处于阻塞状态,也称暂停状态(blocked),blocked结束后,进入runnable状态,等待系统重新分配资源
1.线程调用sleep
2.线程调用阻塞IO流
3.试图获得同步监视器
4.线程等待某个通知
5.调用了suspend方法挂起线程,此方法容易死锁,尽量避免
死亡:
1.run或者call方法执行完成,线程正常结束
2.线程抛出一个未捕获的Exeption或者Error
3.直接调用线程的stop方法来结束该线程,容易导致死锁,不推荐使用。
同步监测器概念解释:简单地说就是同步锁对象,监视器用来确保同一时间只能有一个线程可以访问特定的数据和代码,例如synchronized(Object obj){}中的obj,就是一个同步锁对象,或者叫同步监测器。
下图概述了线程的各个方法与线程状态之间的关系,只需要理解即可(有些方法本文没有提到可以不看):
3.1.3 生产者/消费者模式实现
生产者/消费者模式和多线程的关系可以参考:
Java多种方式解决生产者消费者问题(十分详细)
1) 一生产者一消费者(操作值)
操作值就是说消费的数据是一个值,例如下方代码中的value变量;
//用来存储值的对象
public class ValueObject {
public static String value = "";
}
//生产者
功能实现类
public class P {
private String lock;
public P(String lock) {
//传入所对象
super();
this.lock = lock;
}
public void setValue() {
try {
synchronized (lock) {
if (!ValueObject.value.equals("")) { //当消费品value不等于空时进入等待,就是说消费品没有被消费
lock.wait();
//进入等待,当收到通知的时候会被唤醒
}
//nanoTime的单位是纳秒,描述进程已运行的时间
String value = System.currentTimeMillis() +
"_" + System.nanoTime();
System.out.println("set的值是" + value);
ValueObject.value = value;
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者功能实现类
public class C {
private String lock;
public C(String lock) {
super();
this.lock = lock;
}
public void getValue() {
try {
synchronized (lock) {
if(ValueObject.value.equals(""
) { //当消费品value等于空时进入等待,就是说消费品已经被消费或还没有被生产
lock.wait(); //进入等待,当收到通知的时候会被唤醒
}
System.out.println("get 的值是
:" + ValueObject.value
);
ValueObject.value
= "";
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//生产者线程
public class ThreadP extends Thread {
private P p;
public ThreadP(P p) { //传入生产者功能实现类的对象,方便调用生产方法
super();
this.p = p;
}
@Override
public void run() {
while(true){
p.setValue(); //生产了新的value值
}
}
}
//消费者线程
public class ThreadC extends Thread {
private C c;
public ThreadP(C c) { //传入消费者功能实现类的对象,方便调用消费方法
super();
this.c = c;
}
@Override
public void run() {
while(true){
c.getValue(); //消费了value,方法中value被置为""
}
}
}
public class Run {
public static void main(String[] args){
String lock = new String(""); //锁对象
P p = new P(lock);
//生产者对象,传入锁对象用于同步块
C r = new C(lock);
//消费者对象
ThreadP pThread = new ThreadP(p); //传入生产者对象
ThreadC rThread = new ThreadC(r);
//传入消费者对象
pThread.start();
rThread.start();
}
}
2)多生产者多消费者(操作值)
对一生产者一消费者的实现如果只修改主函数:
//创建多个生产者和多个消费者线程
public class Run {
public static void main(String[] args){
String lock = new String("");
P p = new P(lock);
C r = new C(lock);
ThreadP[] pThread = new ThreadP[2];
ThreadC[] rThread = new ThreadC[2];
for (int i = 0; i < 2; i++) {
pThread[i] = new ThreadP(p);
pThread[i].setName("生产者" + (i + 1));
rThread[i] = new ThreadC(r);
rThread[i].setName("消费者" + (i + 1));
pThread[i].start();
rThread[i].start();
}
Thread.sleep(5000);
//等待上面的线程运行结束
Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(threadArray);
for (int i = 0; i < threadArray.length; i++) {
System.out.println(threadArray[i].getName() + ""
+ threadArray[i].getState());
}
}
}
则运行会出现假死问题,假死就是最后所有线程都会进入等待状态;
造成假死的原因:首先期望的效果是生产者线程生产数据后 notify() 唤醒的是一个消费者线程,但是由于生产者和消费者用的是同一个锁(也必须是同一个锁),一个生产者线程也可能唤醒的是其他生产者线程,这样会导致生产者线程互相唤醒而更多的消费者线程进入等待,这样数据就无法被消费,最后生产者线程也全部进入等待。同理,消费者互相唤醒也会造成一样的结果。
解决方法如下:
//生产者
public class P {
private String lock;
public P(String lock) {
super();
this.lock = lock;
}
public void setValue() {
try {
synchronized (lock) {
while (!ValueObject.value.equals("")) {
System.out.println("生产者 " + Thread.currentThread().getName() + "WAITING了");
lock.wait();
}
System.out.println("生产者 " + Thread.currentThread().getName() + "RUNNABLE了");
String value = System.currentTimeMillis() +
"_" + System.nanoTime();
ValueObject.value = value;
//如果下面使用notify,会因为该方法的随机性(一直只唤醒生产者或消费者)导致生产者或消费者累积,造成“假死”现象
lock.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者
public class C {
private String lock;
public C(String lock) {
super();
this.lock = lock;
}
public void getValue() {
try {
synchronized (lock) {
while(ValueObject.value.equals(""
) {
System.out.println("消费者 " + Thread.currentThread().getName() + "WAITING了");
lock.wait();
}
System.out.println("消费者 " + Thread.currentThread().getName() + "RUNNABLE了");
ValueObject.value
= "";
lock.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//生产者线程
public class ThreadP extends Thread {
private P p;
public ThreadP(P p) {
super();
this.p = p;
}
@Override
public void run() {
while(true){
p.setValue();
}
}
}
//消费者线程
public class ThreadC extends Thread {
private C r;
public ThreadC(C r) {
super();
this.r = r;
}
@Override
public void run() {
while(true){
r.getValue();
}
}
}
//主线程
public class Run {
public static void main(String[] args){
String lock = new String("");
P p = new P(lock);
C r = new C(lock);
ThreadP[] pThread = new ThreadP[2];
ThreadC[] rThread = new ThreadC[2];
for (int i = 0; i < 2; i++) {
pThread[i] = new ThreadP(p);
pThread[i].setName("生产者" + (i + 1));
rThread[i] = new ThreadC(r);
rThread[i].setName("消费者" + (i + 1));
pThread[i].start();
rThread[i].start();
}
Thread.sleep(5000);
//等待上面的线程运行结束
Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(threadArray);
for (int i = 0; i < threadArray.length; i++) {
System.out.println(threadArray[i].getName() + ""
+ threadArray[i].getState());
}
}
}
涉及到线程组,可以参考线程组ThreadGroup
3)一生产一消费(操作栈)
类似操作值的实现,此处略
4)多生产多消费(操作栈)
类似操作值的实现,一生产者/消费者改为多生产者/消费者也会出现假死和wait条件改变等问题
3.1.4 通过管道进行线程间通信
管道流(pipeStream)是一种特殊的流,用于在不
同线程间直接传送数据。一个线程发送数据到输岀管道,另一个线程从输入管道中读数据。
可以通过使用管道,实现不同线程间的通信,而无须借助于类似临时文件之类的东西。
在Java的JDK中提供了 4个类来使线程间可以进行通信:
1) PipedlnputStream 和 PipedOutputStream
——支持字节流
2) PipedReader 和 PipedWriter ——支持字符流
public class WriteData {
public void writeMethod(PipedOutputStream out) {
try {
System.out.printin("write :”);
for (int i = 0; i < 300; i++) {
String outData = "" + (i + 1);
out.write(outData.getBytes());
//字节流
//
out.write(outData);
支持字符流
System.out.print(outData):
}
System.out. println();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ReadData {
public void readMethod(PipedInputStream input) {
try {
System.out.printin("read :");
byte[]
byteArray = new byte[20]; //字节流
//char[] byteArray = new char[20]; 支持字符流
int readLength = input.read(byteArray); //写线程没有写入时,读线程会阻塞在这里
while (readLength != -1) {
String newData = new String(byteArray, 0, readLength);
System.out.print(newData);
readLength = input.read(byteArray);
}
System.out. println();
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//写线程和读线程省略...
//运行类
public class Run {
public static void main(String[] args) {
try{
//字节流
WriteData writeData = new WriteData();
ReadData readData = new ReadData();
PipedInputStream inputStream= new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
//inputStream.connect(outputStream); 和下面一行等价
outputStream.connect(inputStream);
//字符流
/*
PipedReader inputStream = new PipedReader();
PipedWriter outputStream = new PipedWriter();
//inputStream.connect(outputStream);
outputStream.connect(inputStream);
*/
ThreadRead threadRead = new ThreadRead(readData, inputstream);
threadRead.start();
Thread.sleep(2000);
ThreadWrite threadWrite = new ThreadWrite(writeData, outputstream);
threadWrite.start();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.2 方法join的使用
3.2.1 join使用场景及说明
在很多情况下,父线程创建并启动子线程,如果子线程中要进行大量的耗时运算,父线程往往将早于子线程结束之前结束。这时,如果父线程想等待子线程执行完之后再结束,比如子线程处理一个数据,父线程要取得这个数据的值,就要用到join方法。
join方法的作用是等待线程对象销毁。
class Demo implements Runnable {
public void run() {
for(int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
class JoinDemo {
public static void main(String[] args) throws Exception {
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
t1.join();//t1线程要申请加入进来,运行。临时加入一个线程运算时可以使用join方法。
for(int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
方法join的作用是使所属线程Thread-0正常执行run方法中的任务,而使当前线程(一般是父线程)无限期的阻塞,等待线程Thread-0销毁之后再继续执行当前线程后面的代码。
方法join具有使线程排队运行的作用,有些类似同步的运行效果。
join与synchronized的区别:join内部使用wait方法进行等待,而synchronized关键字使用的是“同步监视器”原理做同步。
3.2.2 方法join与异常
public class ThreadB extends Thread{
@Override
public void run(){
try{
ThreadA a = new ThreadA();
a.start;
a.join(); //主线程中调用threadB.interrupt()时会引起异常
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
方法join()与interrupt()方法如 果彼此遇到,则会出现异常。
3.2.3 join(long)与sleep(long)的区别
join(long)的内部是使用wait(long)实现的,两者的区别实际上就是wait(long)和sleep(long)的区别,前者释放锁,而后者不释放锁。
3.3 类ThreadLocal的使用
类ThreadLocal主要解决的就是每个线程绑定自己的值,使每个线程都有自己的变量,并且该变量对于其他线程而言是隔离的。
注意:ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。
参考文章:ThreadLocal就是这么简单
简单使用参考下面这篇:
ThreadLocal的使用及实现
3.3.1 ThreadLocal实现原理
1)每个Thread维护着一个ThreadLocalMap的引用
2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
3)调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
4)调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
5)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
3.3.2 解决初始get()返回null问题
自定义类继承ThreadLocal重写initialValue方法
public class ThreadLocalExt extends ThreadLocal {
@Override
protected Object initialValue() {
return "默认值";
}
}
3.3.3 内存泄漏
下图为对象应用关系
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
想要避免内存泄露就要手动remove()掉。
3.4 类InheritableThreadLocal的使用
使用类InheritableThreadLocal可以在子线程中取得
父线程继承下来的值。
声明在父线程外,父子线程共用一个对象时有效。
public class InheritableThreadLocalExt extends
InheritableThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
@Override
protected Object childValue(Object parentValue)
{
return parentValue+ "子线程获取到的新值”;
}
}
四、Lock的使用
4.1 使用ReentrantLock类
4.1.1 基本使用
在java多线程中,可以使用synchronized关键字来实现线程之间的互斥,但在JDK1.5中新增了ReentrantLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,并且在使用上也比synchronized更加灵活。
Object object=new Object();
void show(){
synchronized (object){
code...//同步代码块或者同步函数对于锁的操作是隐式的
}
}
//jdk1.5之后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作
Lock lock=new ReentrantLock();
void show() {
try {
lock.lock();//获取锁
//do something
} catch(InterruptedExpection e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
注意的是如果在需要同步的代码code处抛出异常的话,后面的释放锁的操作就不会执行,但是释放锁这个动作一定要完成,所以放在finally语句里。
4.1.2 使用Condition实现等待/通知
关键字synchronized与wait()和notify()/notifyAll()方法相结合实现等待/通知模式,类ReentrantLock也可以实现同样的功能,但需要借助Condition对象。
Condition对象是JDK5中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度上更加灵活。
使用ReentrantLock结合Condition类可以实现“选择性通知”;
而synchronized结合wait/notify方式中,synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有线程都注册到它的一个对象身上。
线程开始notifyAll时,需要通知所有的WAITING线程,会出现相当大的效率问题。
Object类中的wait()、notify()、notifyAll()分别对应Condition类中的await()、signal()、signalAll()方法。
ReentrantLock结合Condition类使用示例如下:
class Resource {
private String name;
private int count = 1; //用来区分
private boolean flag; //用来模拟产品的状态
Lock lock=new ReentrantLock();
//通过Condition对象创建两个监视器,一组监视生产者,一组监视消费者,线程用的同一个锁,但是监视器不同,提高了效率。
Condition pro=lock.newCondition();
Condition con=lock.newCondition();
public void set(String name){
lock.lock();
try {
while (flag) { //flag是true时继续等待,代表产品生产后还没有被消费
try {
pro.await(); //相当于wait,但是线程进入等待后是在pro的预执行队列中,
//也就是说只会被pro调用的signal和signalAll方法通知到
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count ++;
System.out.println(Thread.currentThread().getName() + "...生产..." + this.name);
flag = true;
con.signal(); //相当于notify,但是只通知到con对象的等待线程
}finally {
lock.unlock();
}
}
public void get(){
lock.lock();
try {
while (!flag){ //flag是false时继续等待,代表产品还没有生产或已经被消费
try {
con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "...消费..." + this.name);
flag = false;
pro.signal();
}finally {
lock.unlock();
}
}
}
//测试线程和运行类略...
4.1.3 公平与非公平锁
公平与非公平锁:锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺 序是按照线程加锁的顺序分配,即FIFO先进先出顺序。而非公平锁就是一 种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这 个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
//new ReentrantLock()默认false非公平锁
lock = new ReentrantLock(boolean isFair);
4.1.4 常用API
方法名 | 方法描述 |
---|---|
int getHoldCount() | 査询当前线程保持此锁定的个数,也就是调用 lock()方法的次数。(例如锁重入情况) |
int getQueueLength() | 返回正等 待获取此锁定的线程估计数,比如有5个线程,1个线程首先执行await()方法,那么在调用 getQueueLength() 方法后返回值是4,说明有4个线程同时在等待lock的释放。 |
int getWaitQueueLength (Condition condition) | 返回等待与此锁定相关的给定条件Condition的线程估 计数,比如有5个线程都执行了同一个condition 对象的 await() 方法,则调用 getWaitQueueLength() 方法时返回的int值是5。 |
boolean hasQueuedThread (Thread thread) | 査询指定的线程是否正在等待 获取此锁定。 |
boolean hasQueuedThreads() | 査询是否有线程正在等待获取此锁定。 |
boolean hasWaiters (Condition condition) | 査询是否有线程正待等待与此锁定有关的condition条件。 |
boolean isFair() | 判断是不是公平锁。 |
boolean isHeldByCurrentThread() | 判断当前线程是否保持此锁定。 |
boolean isLocked() | 判断此锁定是否有任意线程保持。 |
void lockInterruptibly() | 若当前线程未被中断,则获取锁定,如果已被中断则出现异常。 |
boolean tryLock() | 仅在调用时锁定未被另一个线程保持的情况下,才 获取该锁定。 |
boolean tryLock(long timeout,TimeUnit unit) | 如果锁定在给定等待时 间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。 |
void awaitUninterruptibly() | 代替await(),condition.awaitUninterruptibly()执行后,线程调用interrupt中断不会引发异常。 |
void awaitUntil(long timestamp) | 代替await(),condition.awaitUntil()执行后,如果线程在给定时 间戳到达后仍未被唤醒则自动唤醒。 |
4.2 使用ReentrantReadWriteLock类
类ReentrantLock具有完全互斥排他的效果,即同一时间只有一个线程在执行
ReentrantLock.lock()方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效
率却是非常低下的。
在JDK中提供了一种读写锁ReentrantReadWriteLock类,使
用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁
ReentrantReadWriteLock来提升该方法的代码运行速度。
读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是说多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。
在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。
ReentrantReadWriteLock类的读写锁使用示例如下:
try{
lock.readLock().lock(); //读锁加锁
//lock.writeLock().lock(); //写锁加锁
//do something
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock(); //读锁解锁
//lock.writeLock().unlock(); //写锁解锁
}
五、定时器Timer
5.1 定时器Timer的使用
Timer类主要负责计划任务,也就是在指定的时间开始执行某一个任务。默认情况下不是守护线程。
通过schedule(TimerTask task, Date firstTime)方法计划任务。若计划时间早于当前时间,则立即执行。
Timer构造方法:
TimerTask声明:
TimerTask是以队列的方式一个个地被顺序执行,因此执行的时间有可能和预期的时间不一致。
因为前面的任务有可能消耗的时间较长,则后面的任务运行的时间也会被延误。被延误的任务在上一个任务完成后立即执行。
Timer timer = new Timer("定时器名称",false);
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("do run");
this.cancel(); //TimerTask类中的cancel()方法的作用是将自身从任务队列中清除,也可以在外部调用
}
};
//parse方法非线程安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = null;
try {
date = sdf.parse("2020-06-01 16:00:00"); //若计划时间早于当前时间则不会执行,即shedule不具有追赶执行性
} catch (ParseException e) {
e.printStackTrace();
}
timer.schedule(task, date);
schedule(TimerTask task, Date time):在指定的日期之后执行一次TimerTask任务。
schedule(TimeTask task, Date firstTime, long period):在指定的日期之后,按指定的时间间隔周期性地无限循环执行某一任务。
schedule(TimerTask task, long delay):以执行方法时的时间为参考时间,在此时间基础上延迟指定
的毫秒数后执行一次TimerTask任务。
schedule(TimerTask task, long delay, long period):以执行方法时的时间为
参考时间,在此时间基础上延迟指定的毫秒数,再以某一间隔时间无限次数地执行某一任务。
scheduleAtFixedRate(TimerTask task, Date firstTime, long period),作用和schedule(TimerTask task, long delay, long period)类似,具体区别如下:
- 使用schedule方法:如果执行任务的时间没有被延时,那么下一次任务的执行时间参考 的是上一次任务的“开始”时的时间来计算。
- 使用scheduleAtFixedRate方法:如果执行任务的时间没有被延时,那么下一次任务的执 行时间参考的是上一次任务的“结束”时的时间来计算。
- 延时的情况则没有区别,也就是使用schedule或scheduleAtFixedRate方法都是如果执行任 务的时间被延时,那么下一次任务的执行时间参考的是上一次任务“结束”时的时间来计算。
- scheduleAtFixedRate具有追赶执行性,若计划时间早于当前时间,会把这个时间段之间应做的任务“补充性”执行回来。而对于schedule,若计划时间早于当前时间,则不会执行。
5.2 SimpleDateFormat类线程不安全问题
5.1节涉及SimpleDateFormat的使用,要注意多线程中SimpleDateFormat的一些方法是不安全的。
1、format方法线程不安全
1)首先SimpleDateFormat类有一个共享变量calendar,而这个共享变量的访问没有做到线程安全
2)当使用format方法时,实际是给calendar共享变量设置date值,然后调用subFormat将date转化成字符串
2、parse方法也是线程不安全的,parse方法实际调用的是CalenderBuilder的establish方法来进行解析,其方法中主要步骤不是原子操作。
解决方案:
1)将SimpleDateFormat定义成局部变量
2)加一把线程同步锁:synchronized(lock)
3)使用ThreadLocal,每个线程都拥有自己的SimpleDateFormat对象副本
六、单例模式与多线程
本节讲的是单例模式在多线程中的实现。
可以参考设计模式之单例模式
6.1 立即加载/饿汉模式
立即加载/"饿汉模式"是在调用getInstance()方法之前,实例已经被创建。
public class Single {
private static final Single single = new Single();
private Single(){}
public static Single getInstance(){
return single;
}
}
其中single 变量为静态变量,类加载的时候就会立即加载并且创建实例,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的,因此饿汉模式实现单例模式是线程安全的,但是类加载的同时立即实例化对象,会造成资源的浪费。
6.2 延迟加载/懒汉模式
延迟加载就是在调用getInstance()方法的时候实例才会被创建,常见的实现方式就是在getInstance()方法中进行new实例化。延迟加载是在调用方法时实例才会被创建。
public class Single {
private static Single single = null;
private Single(){}
public static Single getInstance(){
if (single == null) {
single = new Single();
}
return single;
}
}
在多线程的情况下会创建出多个对象,为了保证在多线程情况下依然保证同步,需要添加同步操作。可以在方法上加synchronized关键字来实现,但是每个线程都需要同步执行获取实例的方法,执行效率非常的慢。可以采用DCL双检查锁机制来实现多线程环境中延迟加载单例设计模式。
DCL双检查锁机制实现的具体代码如下:
public class Single {
private static volatile Single single = null;
private Single(){}
public static Single getInstance(){
try {
if (single == null){ //解决懒汉式的效率问题,!=null则返回yi
synchronized (Single.class){ //解决线程安全问题
if (single == null) { //获得锁后的判断才能保证单例(只创建一个实例)
single = new Single();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return single;
}
}
6.3 使用静态内置类实现单例模式
public class MyObject {
//内部类方式
private static class MyObjectHandler
{
private static MyObject myObject
= new MyObject ();
}
private MyObject() {
}
public static MyObject getInstance()
{
return MyObjectHandler.myObject;
}
}
静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。
6.4 序列化与反序列化的单例模式实现
import java.io.ObjectStreamException;
import java.io.Serializable;
public class MyObject implements Serializable {
private static final long serialVersionUID = 888L;
//内部类方式
private static class MyObjectHandler {
private static final MyObject myObject = new MyObject();
}
private MyObject() {
}
public static MyObject getInstance () {
return MyObjectHandler.myObject;
}
protected Object readResolve() throws ObjectStreamException {
System.out.println("readResolve");
return MyObjectHandler.myObject;
}
}
public class Run {
public static void main(String[] args) {
try {
MyObject myObject = MyObject .getInstance();
FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));
ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
oosRef.writeObject(myObject);
oosRef.close();
fosRef.close();
System.out.println(myObject.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt"));
ObjectInputStream iosRef = new ObjectInputStream(fisRef);
MyObject myObject = (MyObject)iosRef.readObject();
iosRef.close();
fisRef.close();
System.out.println(myObject.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”,然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。这里要看序列化流才能深入理解原理。
6.5 使用static代码块实现单例模式
public class MyObject {
private static MyObject instance = null;
private MyObject(){}
static {
instance = new MyObject();
}
public static MyObject getInstance() {
return instance;
}
}
6.6 使用enum枚举实现单例模式
枚举enum和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可 以应用其这个特性实现单例设计模式。
public class Singleton {
public enum SingleEnum {
//这个枚举元素,本身就是单例对象
INSTANCE;
private MyObject myObject;
//添加自己需要的操作
public Single(){}
public MyObject getInstance(){
return myObject;
}
}
public static MyObject getInstance(){
return SingleEnum.INSTANCE.getInstance();
}
}
枚举类实现单例实现过程简单;枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞。
七、线程异常处理
7.1 线程异常
MyThread t1 = new MyThread();
t1.setName("A");
//线程对象调用setUncaughtExceptionHandler方法
t1.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程:" + t.getName() + "出现了异常:");
e.printStackTrace();
}
});
t1.start();
//MyThread t2 = new MyThread();
//t2.setName("B");
//t2.start();
方法setUncaughtExceptionHandler()的作用是对指定的线程对象设置默认的异常处理器。
//线程实现类调用setUncaughtExceptionHandler方法
MyThread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程:" + t.getName() + "出现了异常:");
e.printStackTrace();
}
});
MyThread t1 = new MyThread();
t1.setName("A");
t1.start();
MyThread t2 = new MyThread();
t2.setName("B");
t2.start();
方法setDefaultUncaughtExceptionHandler()的作用是为指定线程类的所有线程对象设置 默认的异常处理器。
7.2 线程组异常
默认的情况下,线程组中的一个线程出现异常不会影响其他线程
的运行。
如果想实现线程组内一个线程出现异常后全部线程都停止运行,可以继承ThreadGroup重写uncaughtException方法实现批量停止。
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
@Override
public void uncaughtException(Thread t, Throwable e)
{
super.uncaughtException(t, e);
this.interrupt();
}
}
7.3 异常传递
涉及到线程组,可以参考线程组ThreadGroup,个人觉得这篇也不是太全的但胜在简洁,可以自行查阅其他资料。
测试后有以下结论:当线程和线程组均如前两小节进行了异常处理时,指定的线程对象的异常处理抛出不会被传递到线程组,指定线程类的所有对象的异常处理可以被传递,但需要注意在线程组异常处理中添加 super.uncaughtException(t, e); 才能获取。