线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。多线程程序应用能够在同一时间执行多于一个任务,进而提升整体处理性能。本文针对JAVA中线程模块做了简单的解释与实战,介绍了线程的相关概念,锁的概念,线程间通信的方法等,内容比较简单,但是却比较繁琐,概念性东西比较多,有些内容比较细节,但是却在面试或者使用中是不可忽略的东西。
前言
本文将从最基础的部分开始讲解JAVA线程的知识,最后深入了解JAVA多线程的高级应用以及优化。适用于刚接触JAVA多线程的小伙伴。本文中所涉及的项目以及代码点赞或者评论后均可后台私聊获取,欢迎小伙伴深夜骚扰。
一、线程基础
1、线程和进程的概念
打开一个软件就是一个进程,一个进程分多个线程运行。简单的说,进程是操作系统运行的最小单位,线程是操作系统分配资源的最小单位。举个例子:我们打开QQ.exe,就已经开启了一个进行,我们在QQ上一边视频通话一边打字聊天,这便是多个QQ线程在工作帮我们完成的。
2、多线程
2.1、线程的创建方式
继承Thread类,重写run()方法,调用start()方法启动线程。
实现Runnable接口,重写run()方法,调用start()方法启动线程。
使用线程工具类创建线程。比如:Executors.newFixedThreadPool()创建线程池,调用eecute()执行。
2.2、线程的几个常用方法
currentThread()方法:Thread.currenThread()返回一个当前正在运行的线程对像,通过这个返回值可以获取到当前线程的一些信息,比如:线程名,线程ID等信息。
sleep()方法:Thread.sleep(100)表示让当前正在运行的线程睡眠(暂停)一段时间,单位毫秒。
getId()方法:Thread.currentThread().getId()可获取当前线程的唯一标识。
isAlive()方法:Thread.currentThread().isAlive()用来判断当前线程是否处于活跃状态。如果线程已经启动但未结束,那么线程就处于活跃状态。
示例:
public class Thread01 {
public static void main(String[] args) {
ThreadA ta = new ThreadA();
new Thread(ta, "A").start();
}
}
class ThreadA extends Thread {
public void run() {
System.out.println("当前线程的名字为:" + Thread.currentThread().getName());
System.out.println("当前线程的存活状态为:" + Thread.currentThread().isAlive());
System.out.println("当前线程的ID为:" + Thread.currentThread().getId());
}
}
运行结果:
3、暂停线程
停止线程可以使用stop()、suspend()、resume()三个方法停止线程,但是停止线程可能会造成一些未知的错误,目前JDK中这三个方法已经被标记为已弃用,所以不推荐使用这三个方法停止线程,下边着重介绍interrupt()方法停止线程。
3.1 stop()、suspend()、resume()停止线程
stop()、suspend()、resume()被标记为已弃用的原因分析:
stop()方法暴力停止线程会造成线程并释放锁,如果立即停止线程可能会造成一些清理工作突然停止,无法彻底完成。突然释放锁会导致数据不能及时同步,会出现一些数据不同步的问题。
示例:
// 创建Thread01.java
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
User us = new User();
ThreadA1 ta1 = new ThreadA1(us);
ta1.start();
Thread.sleep(500);
ta1.stop();
System.out.println(us.getName() + " " + us.getPass());
}
}
// 创建ThreadA1.java
class ThreadA1 extends Thread {
private User us;
public ThreadA1(User us) {
this.us = us;
}
@Override
public void run() {
try {
us.setPassAndName("bb", "bb");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 创建实体类User.java
class User {
private String name = "aa";
private String pass = "aa";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPass() {
return pass;
}
public void setPass(String pass) {
this.pass = pass;
}
public synchronized void setPassAndName (String name, String pass) throws InterruptedException {
this.name = name;
Thread.sleep(10000);
this.pass = pass;
}
}
运行结果:
suspend()暂停线程,暂停线程之后,可以用resume()恢复被暂停的线程。暂停线程会造成线程数据不同步的问题,suspend()、resume()的使用还可能会造成公共资源被独占,从而导致其他线程无法访问公共资源。
示例:
// 创建Thread01.java
public class Thread02 {
public static void main(String[] args) throws InterruptedException {
User02 us = new User02();
new Thread(() -> {
System.out.println("线程A启动了");
Thread.currentThread().setName("A");
us.setPassAndName();
System.out.println("线程A结束了");
}).start();
new Thread(() -> {
System.out.println("线程B启动了");
Thread.currentThread().setName("B");
us.setPassAndName();
System.out.println("线程B结束了");
}).start();
}
}
// 创建实体类User02.java
class User02 {
public synchronized void setPassAndName () {
System.out.println("进入同步方法");
if (Thread.currentThread().getName().equals("A")) {
System.out.println("A 线程被 suspend");
Thread.currentThread().suspend();
}
System.out.println("退出同步方法");
}
}
运行结果:
线程A进入同步方法后被suspend了,导致对象被独占,所以线程B只打印了开始,而无法进入同步方法。
3.2 interrupt()停止线程
使用interrupt()停止线程,并不会立即停止线程,以下代码示例将会证明这点:
示例:
// 创建Thread03.java
public class Thread03 {
public static void main(String[] args) throws InterruptedException {
User03 us = new User03();
new Thread(() -> {
System.out.println("线程A启动了");
Thread.currentThread().setName("A");
us.setPassAndName();
System.out.println("线程A结束了");
}).start();
}
}
// 创建实体类User03.java
class User03 {
public synchronized void setPassAndName () {
System.out.println("进入同步方法");
System.out.println("当前线程被interrupt了");
Thread.currentThread().interrupt();
System.out.println("退出同步方法");
}
}
运行结果:
如上结果可以看出,同步方法被interrupt了,但是后边的代码还是被执行打印出来了,这说明interrupt并不是正真的停止线程。
3.3 使用interrupted()和isInterrupted()判断线程状态
示例:更改Thread03.java中的main方法如下
public static void main(String[] args) throws InterruptedException {
User03 us = new User03();
new Thread(() -> {
System.out.println("线程A启动了");
Thread.currentThread().setName("A");
us.setPassAndName();
System.out.println("当前线程的状态为" + Thread.currentThread().isInterrupted());
System.out.println("当前线程的状态为" + Thread.currentThread().interrupted());
System.out.println("线程A结束了");
}).start();
}
运行结果:
由此可以看出,线程被interrupt了之后,线程的确是停止了,因为线程线程状态为interrrupt了,但是后边的语句还是被执行了。
示例:更改Thread03.java中的main方法如下(调换两个输出当前线程状态的语句),结果如下:
// 创建Thread01.java
public class Thread03 {
public static void main(String[] args) throws InterruptedException {
User03 us = new User03();
new Thread(() -> {
System.out.println("线程A启动了");
Thread.currentThread().setName("A");
us.setPassAndName();
System.out.println("当前线程的状态为" + Thread.currentThread().interrupted());
System.out.println("当前线程的状态为" + Thread.currentThread().isInterrupted());
System.out.println("线程A结束了");
}).start();
}
}
运行结果:
调换两个打印线程状态的位置后,发现打印出来的线程状态一个false,一个为ture,这是因为interrupted()方法有线程状态清除的功能,先调用interrupted()方法判断线程状态的确是被interrupt了,所以为true,紧接着interrupted()方法将该该线程的interrupt为true的状态清除掉(如下图中源码中标出的注释),所以后边调用isInterrupted()方法来判断线程状态的时候就为false。
3.4 使用interrupt()方法和抛出异常法停止线程(推荐)
示例:
// 创建Thread04.java
public class Thread04 {
public static void main(String[] args) throws InterruptedException {
User04 us = new User04();
new Thread(() -> {
System.out.println("线程A启动了");
Thread.currentThread().setName("A");
try {
us.setPassAndName();
System.out.println("线程A结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
// 创建实体类User04.java
class User04 {
public synchronized void setPassAndName () throws InterruptedException {
System.out.println("进入同步方法");
System.out.println("当前线程被interrupt了");
Thread.currentThread().interrupt();
throw new InterruptedException();
}
}
运行结果:
使用异常法停止线程,可以看到线程是正真被停止了,“线程A结束了”没有打印出来,说明线程并没有继续执行。
4、线程的优先级
JAVA中的线程具有优先级,优先级越高的线程执行级别就越高,抢占资源的能力就越强。但是优先级高的线程不一定最先执行完,也就是说,线程是否先执行结束与优先级没有关系。JAVA提供setPriority()方法设置线程优先级,范围为1~10。
示例:
/**
* 线程优先级测试类
*/
// 创建Thread05.java
public class Thread05 {
public static void main(String[] args) {
ThreadA05 threadA05 = new ThreadA05();
threadA05.setName("A");
threadA05.setPriority(1);
ThreadB05 threadB05 = new ThreadB05();
threadB05.setName("B");
threadB05.setPriority(10);
threadA05.start();
threadB05.start();
}
}
// 创建ThreadA05.java
class ThreadA05 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行开始");
System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
// 创建ThreadB05.java
class ThreadB05 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行开始");
System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
运行结果:
线程B的优先级被设置为10,线程A的优先级被设置为1,总是线程B先开始执行,但是却是最后一个执行结束的,所以证实线程是否先执行完与优先级没有关系。再次实验发现,当两个线程的优先级差别不是很大的时候,就不会存在优先级大的线程先执行的问题,所以说线程的执行在大多数情况下是随机的。
4.1 线程的优先级具有继承特点
如果设置A线程的优先级为6,再用A线程调用B线程,那么B线程的优先级也是6。
示例:创建如下代码
/**
* 线程优先级继承特性测试类
*/
// 创建Thread06.java
public class Thread06 {
public static void main(String[] args) {
Thread.currentThread().setPriority(6);
ThreadD06 threadD06 = new ThreadD06();
threadD06.setName("D");
threadD06.start();
System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
}
}
// 创建ThreadC06.java
class ThreadC06 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行开始");
System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
// 创建ThreadD06.java
class ThreadD06 extends ThreadC06 {
@Override
public void run() {
ThreadC06 threadC06 = new ThreadC06();
threadC06.setName("C");
threadC06.start();
System.out.println(Thread.currentThread().getName() + "执行开始");
System.out.println(Thread.currentThread().getName() + "的优先级为" + Thread.currentThread().getPriority());
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
运行结果:
设置main线程的优先级为6,main调用D线程,D线程调用C线程,发现优先级具有继承性,mian、D、C三个线程的优先级全部被修改为6。
5、守护线程
守护线程是一直在后台运行的线程,直到所有的非守护线程都不存在了。JVM中一般有两种线程,一种是用户线程,一种是守护线程,最典型的守护线程是垃圾回收线程(GC线程)。守护线程会在最后一个非守护线程结束后而自动销毁。JAVA中提供setDaemon(true/false)来标记线程为守护线程或者是用户线程。源码如下:
二、线程间通信
1、等待/通知(wait()/notify())机制
等待/通知机制就是,某个线程在执行的过程中等待另外一个线程通知它,它获取到通知后就会继续执行。最典型的就是就餐问题,送餐员必须等待厨师通知他可以取餐的时候,他才可以去取餐,否则他只能一直在等待。
wait()/notify()必须在synchronized修饰的方法或者代码中调用,也就是说,必须得有锁对象才能正常使用,否则就会报IllegalMonitorStateException。
示例:
/**
* 等待通知代码测试类
*/
// 创建Thread09.java
public class Thread07 {
public static void main(String[] args) {
Object obj = new Object();
Thread T1 = new Thread(() -> {
System.out.println("T1 start");
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("T1 end");
});
T1.start();
Thread T2 = new Thread(() -> {
System.out.println("T2 start");
synchronized (obj) {
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
obj.notify();
}
System.out.println("T2 end");
});
T2.start();
}
}
运行结果:
T1线程启动后打印了“T1 start”后调用了wait()方法,进入等待队列,T2线程启动后,调用了notify()方法唤醒了T1线程,使得T1线程继续执行,打印了“T1 end”这句话。这就是wait()/notify()最基础的用法。
1.2、wait()执行后会立即释放锁,notify()执行后不立即释放锁,必须等待同步方法或者同步代码块里面的代码执行完才会释放锁。
示例:
/**
* wait()/notify()执行后所释放时间验证类
*/
public class Thread08 {
public static void main(String[] args) {
Object lock = new Object();
ThreadA10 threadA10 = new ThreadA10();
new Thread(() -> {
synchronized (lock) {
Thread.currentThread().setName("T1");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA10.printString();
}
}).start();
new Thread(() -> {
synchronized (lock) {
Thread.currentThread().setName("T2");
threadA10.printString();
lock.notify();
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}).start();
}
}
class ThreadA10 {
public void printString () {
System.out.println(Thread.currentThread().getName());
}
}
运行结果:
T1线程和T2线程都是用了同一把lock锁,T1线程启动后调用了wait()方法,进入等待队列,T2线程启动后,立马打印了“T2”,说明T1线程立马释放了锁被T2线程获得了lock锁。T2调用了notify()方法唤醒了T1线程,但是T1线程并没有立马,而是等到T2线程执行完后边的代码后,T1线程才继续执行,打印了“T1”这句话。这就说明wait()执行后会立即释放锁,notify()执行后不立即释放锁,必须等待同步方法或者同步代码块里面的代码执行完才会释放锁。
1.3、等待通知的经典案例-生产者/消费者模式
生产者/消费者模式指的是,生产者生产好数据后通知消费者消费数据,而消费者消费完数据后,又通知生产者生产数据,这是一个往返循环的过程。生产者/消费者模式在处理过程中也是使用的wait()/notify()机制,但是这中机制可能会造成一种假死的现象。下边模拟这中情况。
示例:
/**
* 生产者/消费者模式(wait()/notify())执行后出现假死现象验证类
*/
public class Thread09 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
P p = new P(lock);
C c = new C(lock);
for (int i = 0; i < 2; i++) {
ThreadA09 threadA09 = new ThreadA09(p);
ThreadB09 threadB09 = new ThreadB09(c);
threadA09.setName("P" + i);
threadB09.setName("C" + i);
threadA09.start();
threadB09.start();
}
Thread.sleep(500);
Thread[] threads = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
Thread.currentThread().getThreadGroup().enumerate(threads);
for (int i = 0; i < threads.length; i++) {
System.out.println(threads[i].getName() + " " + threads[i].getState());
}
}
}
class ValueObject {
public static volatile String value = "";
}
// 生产者
class P {
private Object lock;
public P(Object lock) {
this.lock = lock;
}
public void product() {
synchronized (lock) {
while (!"".equals(ValueObject.value)) {
try {
System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING了");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ValueObject.value = "" + System.currentTimeMillis();
System.out.println("生产者 " + Thread.currentThread().getName() + " RUNNAVBLE了");
lock.notify();
}
}
}
// 消费者
class C {
private Object lock;
public C(Object lock) {
this.lock = lock;
}
public void consume() {
synchronized (lock) {
while ("".equals(ValueObject.value)) {
try {
System.out.println("消费者 " + Thread.currentThread().getName() + " WAITING了");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ValueObject.value = "";
System.out.println("消费者 " + Thread.currentThread().getName() + " RUNNAVBLE了");
lock.notify();
}
}
}
class ThreadA09 extends Thread {
private P p;
public ThreadA09(P p) {
this.p = p;
}
@Override
public void run() {
while (true) {
p.product();
}
}
}
class ThreadB09 extends Thread {
private C c;
public ThreadB09(C c) {
this.c = c;
}
@Override
public void run() {
while (true) {
c.consume();
}
}
}
运行结果:
从结果可以看出,所有的生产者/消费者线程都成WAITING状态了,而程序还是运行状态,线程出现假死状态,出现假死状态的可能原因是:notify()是随机唤醒一个等待的线程,有可能是“生态者”唤醒“生态者”或者是“消费者”唤醒“消费者”,不停的这样操作,导致最后所有的线程都成WAITING状态,出现假死装状态。
从上边的分析可以看出,假死的原因就是因为因为不断的唤醒了同类,解决的办法就是将lock.notify()换成lock.notifyAll()。每次唤醒的线程的时候,将所有的等待的线程全部唤醒。将lock.notify()换成lock.notifyAll()后,会发现程序将会不停的执行下去。
1.4、 等待/通知通信模式实战
使用等待/通知模式实现交叉备份数据的需求。要求:起10个线程,5个线程备份数据到数据库A,5个线程备份数据到数据库B,并且线程之间交叉执行备份动作。
示例:
/**
* 等待/通知m模式实战:交叉备份数据
* 要求:起10个线程,5个线程备份数据到数据库A,5个线程备份数据到数据库B
* 并且要求线程交叉执行
*/
public class Thread11 {
public static void main(String[] args) {
BckDada bckDada = new BckDada();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
bckDada.BckDadaA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
bckDada.BckDadaB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
class BckDada {
private volatile boolean isStartBckA = false;
public synchronized void BckDadaA () throws InterruptedException {
while (isStartBckA) {
this.wait();
}
System.out.println("######");
isStartBckA = true;
this.notifyAll();
}
public synchronized void BckDadaB () throws InterruptedException {
while (!isStartBckA) {
this.wait();
}
System.out.println("&&&&&&");
isStartBckA = false;
this.notifyAll();
}
}
运行结果:
注意:实现的关键在于private volatile boolean isStartBckA = false这个变量。
2、使用管道进行通信
管道(pipeStream)是是一种特殊的输入/输出流,用于不同线程间传输数据,一个线程发送数据到输出管道中,另外一个线程从输入管道中获取数据。
JAVA提供了四个类来实现管道通信:
字符流管道:PipedWriter、PipedRead
字节流管道:PipedOutputStream、PipedInputStream
示例:
/**
* 线程间管道通信测试类
* 1、字节流方式
* 2、字符流方式
*/
public class Thread10 {
public static void main(String[] args) throws InterruptedException {
// 管道:字节流方式
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
// 管道:字符流方式
PipedWriter pipedWriter = new PipedWriter();
PipedReader pipedReader = new PipedReader();
try {
pipedWriter.connect(pipedReader);
// pipedReader.connect(pipedWriter);
inputStream.connect(outputStream);
// outputStream.connect(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
PipelineReadData pipelineReadData = new PipelineReadData();
new Thread(() -> pipelineReadData.readData(inputStream)).start();
new Thread(() -> pipelineReadData.readData(pipedReader)).start();
Thread.sleep(500);
PipelineWriteData pipelineWriteData = new PipelineWriteData();
new Thread(() -> pipelineWriteData.writeData(outputStream)).start();
new Thread(() -> pipelineWriteData.writeData(pipedWriter)).start();
}
}
class PipelineReadData {
public void readData (PipedInputStream inputData) {
try {
byte[] bytes = new byte[1024];
int dataLength = inputData.read(bytes);
System.out.println("readData read (字节流方式):");
while (-1 != dataLength) {
System.out.println(new String(bytes, 0, dataLength));
dataLength = inputData.read(bytes);
}
inputData.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void readData (PipedReader inputData) {
try {
char[] chars = new char[1024];
int dataLength = inputData.read(chars);
System.out.println("readData read (字符流方式):");
while (-1 != dataLength) {
System.out.println(new String(chars, 0, dataLength));
dataLength = inputData.read(chars);
}
inputData.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class PipelineWriteData {
public void writeData (PipedOutputStream outputData) {
try {
System.out.println("writeData write (字节流方式):");
System.out.println("hello word!");
outputData.write("hello word!".getBytes());
outputData.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void writeData (PipedWriter outputData) {
try {
System.out.println("writeData write (字符流方式):");
System.out.println("hello word!");
outputData.write("hello word!");
outputData.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
3、使用join()方法
如果一个线程需要等待另外一个线程运行完毕后采取执行,那么就得使用join()方法,join()方法的意思就是把A线程加入到B线程的内部,相当于将A线程变成了B线程的子线程,那么此时B线程就必须等待A线程执行完才能执行后边的代码,此时B线程是就可以使用A线程执行完的数据。总之:
join()方法有使线程排队的作用,类似于synchronized达到同步执行的效果,只是join()底层是wait()实现,synchronized底层是使用监视器实现。
join()方法可以完成线程间数据的可见性。
join(long)方法底层使用wait(long)方法实现,具有是线程等待一段时间的作用,sleep(long)也能达到这个效果,只是join(long)执行后会释放锁,而sleep(long)不会释放锁。
join()方法执行完后,具有释放锁的特点。
示例:
/**
* Thread.join()方法测试类
*/
public class Thread12 {
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
System.out.println("T1 start");
}, "T1");
Thread t2 = new Thread(() ->{
try {
t1.join(1000); // 这段代码可以保证先打印T1 start后打印T2 start
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T2 start");
}, "T2");
t2.start();
t1.start();
}
}
运行结果:
注意:使用join()方法可能会导致join()之后的代码先运行(出现意外的情况下)。
三、ThreadLocal的使用
1、ThreadLocal具有线程隔离性
如果多个线程都往同一个public static的ThreadLocal的变量中存储数据,那么对应的线程的数据是和线程本身挂钩的,也就是在A线程中不会获取到B线程的数据,在B线程中不会获取到A线程的数据。如下验证:
示例:
/**
* ThreadLocal的数据隔离性验证
*/
public class Thread13 {
public static void main(String[] args) {
CommData.local.set("main set value");
Thread t1 = new Thread(() -> {
CommData.local.set("T1 set value");
System.out.println("T1 get value" + CommData.local.get());
}, "T1");
Thread t2 = new Thread(() -> {
CommData.local.set("T2 set value");
System.out.println("T2 get value" + CommData.local.get());
}, "T2");
t1.start();
t2.start();
System.out.println("main get value" + CommData.local.get());
}
}
class CommData {
public static ThreadLocal<String> local = new ThreadLocal<>();
}
运行结果:
从结果中可以看出,在当前线程中使用CommData.local.get()只能获取当前线程存储的内存,其他线程存储的线程对于当前线程是不可见的。
2、InheritableThreadLocal的数据继承性验证
InheritableThreadLocal可以调用initialValue()设置初始值,就算没有set,也不会获取到null。
InheritableThreadLocal在父线程设置的值,可以继承到子线程中使用,但是在子线程设置的值,不会传递到父线程,父线程依旧取得值是旧值。
示例:
/**
* InheritableThreadLocal的数据继承性验证
* 1、InheritableThreadLocal可以调用initialValue()设置初始值,就算没有set,也不会获取到null
* 2、InheritableThreadLocal在父线程设置的值,可以继承到子线程中使用,但是在子线程设置的值,不会传递到父线程,
* 父线程依旧取得值是旧值
*/
public class Thread14 {
public static void main(String[] args) {
CommData14 commData14 = new CommData14();
// commData14.set("main set value");
Thread t1 = new Thread(() -> {
commData14.set("T1 set value");
System.out.println("T1 get value " + commData14.get());
}, "T1");
Thread t2 = new Thread(() -> {
System.out.println("T2 get value " + commData14.get());
}, "T2");
t1.start();
t2.start();
System.out.println("main get value " + commData14.get());
}
}
class CommData14 extends InheritableThreadLocal {
@Override
protected Object initialValue() {
return new Date().getTime();
}
@Override
protected Object childValue(Object parentValue) {
return parentValue + " child set value……";
}
}
运行结果:
打开main()方法中的第二行注释后,运行结果如下:
这说明在InheritableThreadLocal中,子线程中设置的值,不会流传到其他线程中(父线程和其他线程依旧获取的是初始值),而父线程设置的值可以继承至其他所有子线程中(所有的线程都获取到了父线程中设置的值)。
四、锁的使用
1、ReentrantLock锁(可重入锁)
ReentrantLock锁可以达到和synchronized同样的效果,甚至能比synchronized更加灵活。ReentrantLock锁使用lock()和unlock()解锁和释放锁。
1.1、PeentrantLock锁中的公平锁和非公平锁
PeentrantLock锁分公平锁和非公平锁,提供两个构造函数:new PeentrantLock()的时候默认是非公平锁,new PeentrantLock(boolean fair)的时候,接受一个boolean值来指定。
示例:
/**
* ReentrantLock(可重入锁)加锁保证线程之间顺序执行
*
* ReentrantLock内部类抽象类Sync
* -
* -
* ReentrantLock Sync继承于AQS Sync的实现类NonfairSync
* lock() -----调用----> lock() ---------> lock()
*/
public class Thread15 {
//private static final ReentrantLock lock = new ReentrantLock();
private static final ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(Thread15::printString, "A" + i).start();
}
}
public static void printString() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "被执行了");
} finally {
lock.unlock();
}
}
}
运行结果:
非公平锁:创建非公平锁new ReentrantLock()后,加上lock.lock()和lock.unlock()方法后依旧是乱序打印,这说明非公平锁并不是按照执行启动顺序来获取锁的,先启动的并不一定会先获得锁。
公平锁:创建公平锁new ReentrantLock(true)后,加上lock.lock()和lock.unlock()方法后基本上呈现有序打印,这说明公平锁在一定程度上可以保证,先到的定会先获得锁。
设置公平锁的时候,打印顺序基本上是按照启动顺序执行的。当然相反的设置非公平锁的时候,一定程度上,后边启动的线程会比它前边启动的线程先执行。
2、PeentrantLock中的Condition
使用Condition可以实现等待/通知模式。利用Condition可以实现顺序打印。
示例:
/**
* Condition实现等待/通知模式
* 1、使用Condition实现将业务排序规划
*/
public class Thread18 {
public static volatile int nextPrint = 1;
public static void main(String[] args) throws InterruptedException {
ThreadA18 threadA18 = new ThreadA18();
for (int i = 0; i < 5; i++) {
new Thread(() -> threadA18.aPrintString(nextPrint), "A").start();
new Thread(() -> threadA18.bPrintString(nextPrint), "B").start();
new Thread(() -> threadA18.cPrintString(nextPrint), "C").start();
}
}
}
class ThreadA18 {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition conditionA = lock.newCondition();
private static final Condition conditionB = lock.newCondition();
private static final Condition conditionC = lock.newCondition();
public void aPrintString(int nextPrint) {
try {
lock.lock();
if (nextPrint != 1) {
conditionA.await();
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
Thread18.nextPrint = 2;
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void bPrintString(int nextPrint) {
try {
lock.lock();
if (nextPrint != 2) {
conditionB.await();
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
Thread18.nextPrint = 3;
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void cPrintString(int nextPrint) {
try {
lock.lock();
if (nextPrint != 3) {
conditionC.await();
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
Thread18.nextPrint = 1;
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果:
使用Condition实现了线程顺序执行,保证了线程按照我们的规划执行响应的功能。Condition可以灵活的设置不同的线程锁,可以实现针对性对线程控制的能力,在使用上更加灵活。
3、ReentrantReadWriteLock锁(读写锁)
ReentrantLock在加锁的过程中,是完全排他的效果,即同一个时间,只能允许一个线程执行lock()方法后边的代码,有时候在不需要操作变量,而只是读取的情况下,ReentrantLock效率就比较低,JAVA中提供了一种读写锁(ReentrantReadWriteLock),读锁与写锁是两种锁,可以做到,在只有读操作时候,可以保证多个线程同步执行,在写操作的过程只能一步执行。
示例:
/**
* ReentrantReadWriteLock读写锁测试类
* 1、读读共享
* 2、读写/写读互斥
* 3、写写互斥
*/
public class Thread19 {
public static void main(String[] args) {
ThreadA19 threadA19 = new ThreadA19();
new Thread(threadA19::read, "A").start();
new Thread(threadA19::read, "B").start();
new Thread(threadA19::write, "C").start();
new Thread(threadA19::write, "D").start();
}
}
class ThreadA19 {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read () {
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + " read " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
readWriteLock.readLock().unlock();
}
public void write () {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + " write " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
readWriteLock.writeLock().unlock();
}
}
运行结果:
从结果可以看出,A和B两个线程几乎是同时执行read()方法中的代码的。因为他们打印的时间几乎是相同的,而C和D两个线程是异步执行的,因为他们打印的时间相差了1000毫秒左右,这是因为D线程执行的过程中必须等待C线程执行完释放速锁之后才能执行。
五 定时器Timer
JAVA中的定时器Timer是一个负责执行计划任务的功能,即在某个时间开始执行某一个任务。Timer最主要的是负责计划任务,具体的任务的实现需要放在TimerTask的子类中,TimerTask是一个实现了Runnable接口的抽象类,所以需要重写它的run()方法。
1 schedule()方法
schedule()方法在Timer类中,几个重载的方法。
1.1 schedule(TimerTask task, long delay)
该方法以启动的时间为主,延迟指定的毫秒数后在执行相关的代码。
示例:
/**
* Timer定时器测试验证类
*/
public class Thread20 {
public static void main(String[] args) {
Timer timer = new Timer();
System.out.println("***************************");
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("task run time" + new Date());
}
};
System.out.println("task start time" + new Date());
// 该任务启动后,延迟2秒后再执行
timer.schedule(task, 2000);
}
}
运行结果:
可以看到,任务是在启动后,延迟了2秒后才去执行的。
1.2 schedule(TimerTask task, long delay, long period)
该任务启动后,延迟指定的毫秒数后执行一次,之后以1000毫秒为间隔,无限重复执行
示例:
/**
* Timer定时器测试验证类
*/
public class Thread19 {
public static void main(String[] args) {
Timer timer = new Timer();
System.out.println("***************************");
TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("task2 run time" + new Date());
}
};
System.out.println("task2 start time" + new Date());
// 该任务启动后,延迟指定的时毫秒数后执行一次,之后以1000毫秒为间隔,无限重复执行
timer.schedule(task2, 2000, 1000);
}
}
运行结果:
1.3 schedule(TimerTask task, Date time)
该方法以启动的时间为主,在指定的时间执行相关的代码,如果时间在过去,则计划立即执行任务。
示例:
/**
* Timer定时器测试验证类
*/
public class Thread20 {
public static void main(String[] args) {
Timer timer = new Timer();
System.out.println("***************************");
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("task run time" + new Date());
}
};
System.out.println("task start time" + new Date());
// 该任务启动后,再指定的时间执行相关的任务,如果指定的时间为过去,那么会立即执行。
// timer.schedule(task, new Date(System.currentTimeMillis() + 2000));
timer.schedule(task, new Date(System.currentTimeMillis() - 2000));
}
}
运行结果:
1.4 schedule(TimerTask task, Date time, long period)
该任务启动后,在指定的时间(如果是过去,则立马执行)执行一次后,之后以1000毫秒为间隔,无限重复执行
示例:
/**
* Timer定时器测试验证类
*/
public class Thread21 {
public static void main(String[] args) {
Timer timer = new Timer();
System.out.println("***************************");
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println("task1 run time" + new Date());
}
};
System.out.println("task1 start time" + new Date());
// 该任务启动后,在指定的时间(如果晚于当前时间,则立马执行,否则等到指定时间点时才执行)执行一次后,之后以1000毫秒为间隔,无限重复执行
timer.schedule(task1, new Date(System.currentTimeMillis() + 5000), 1000);
// timer.schedule(task1, new Date(System.currentTimeMillis() + 5000), 1000);
}
}
运行结果:
2 scheduleAtFixedRate()方法
用法参考schedule()方法。schedule()方法和scheduleAtFixedRate()方法不需要考虑线程安全问题,因为任务都是按照顺序放到队列中,也会按照顺序执行。
3 cancel()方法
Timer类中cancel()方法,会将所有的任务队列中的任务全部清除掉(但有时也会出现无法清除的现象,导致执行cancel()后,任务依旧会执行),然后自动停止程序。 TimerTask类中的cancle()方法只是清除自身的任务,而不会影响其他任务,程序不会退出。
示例:
/**
* Timers类中cancel()方法测试验证类
*/
public class Thread22 {
public static void main(String[] args) {
Timer timer = new Timer();
System.out.println("***************************");
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("task run time" + new Date());
}
};
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println("task1 run time" + new Date());
}
};
// 该任务启动后
timer.schedule(task, 2000);
timer.schedule(task1, 2000);
// task.cancel();
timer.cancel();
}
}
运行结果(放开timer.cancel());
运行结果(放开task.cancel());
六 单例模式与多线程
1 饿汉模式单例
饿汉模式在使用类的时候就已经new出来一个对象的实例,不管有没有用到。这种情况下,对象一直存在内存中,有点占用资源。
实例:
/**
* 单例模式(饿汉式)
*/
public class Thread23 {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(
Thread.currentThread().getName() + " get: " + SingleModel23.getSingleModel23().hashCode()),
"Thread" + i).start();
}
}
}
class SingleModel23 {
private static final SingleModel23 singleModel23 = new SingleModel23();
private SingleModel23 () {}
public static SingleModel23 getSingleModel23 () {
return singleModel23;
}
}
运行结果:
从打印结果来看,所有的hashcode都是同一个值,可以确认实现了饿汉式单例模式。但是由于getSingleModel22()方法没有同步,也有可能会出现线程不安全问题。
2 懒汉模式单例(DCL版,推荐)
懒汉模式单例在使用类的时候,不会立即new出来对象的实例,而是在用到的时候,才开始new,这样可以避免浪费内存的问题。
需要对对象实例需要增加volatile关键字。
增加双重检查锁实现,避免线程不安全问题
示例:
/**
* DCL单例模式(懒汉式)
*/
public class Thread24 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(
Thread.currentThread().getName() + " get: " + SingleModel.getSingleModelInstance().hashCode()),
"Thread" + i).start();
}
}
}
class SingleModel {
private volatile static SingleModel singleModel;
private SingleModel() {
}
public static SingleModel getSingleModelInstance() {
if (singleModel == null) {
synchronized (SingleModel.class) {
if (singleModel == null) {
singleModel = new SingleModel();
}
}
}
return singleModel;
}
}
运行结果:
从打印结果来看,所有的hashcode都是同一个值,可以确认实现了懒汉式单例模式。由于getSingleModelInstance ()方法DCL机制,所以基本上避免了现线程不安全问题。DCL版本的懒汉式单例模式也是最常用的设计模式。
3 静态内部类实现单例模式
在JAVA中,静态代码,静态域只会在使用类的时候执行一次,所以一定是线程安全的。所以可以使用静态内部类来实现单例模式。
示例:
/**
* 静态内部类实现单例模式
*/
public class Thread25 {
public static class InnerThread25 {
private static final Thread25 thread25 = new Thread25();
}
public static Thread25 getInstance() {
return InnerThread25.thread25;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> System.out.println(
Thread.currentThread().getName() + " get: " + Thread25.getInstance().hashCode()),
"Thread" + i).start();
}
}
}
运行结果:
从打印结果来看,所有的hashcode都是同一个值,可以确认实现了饿汉式单例模式。由于静态内部类只会执行一次,所以基本上避免了现线程不安全问题。
4 使用static代码块实现单例模式
和静态内部类一样,静态代码块在类使用的时候,静态代码块只会执行一次,所以,静态代码块也可以实现单例模式,并且还不用担心线程不安全的问题。
示例:
/**
* 使用静态代码块实现单例模式
*/
public class Thread26 {
private static Thread26 thread26;
static {
thread26 = new Thread26();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> System.out.println(
Thread.currentThread().getName() + " get: " + thread26.hashCode()),
"Thread" + i).start();
}
}
}
运行结果:
从打印结果来看,所有的hashcode都是同一个值,可以确认实现了饿汉式单例模式。由于静态代码块只会执行一次,所以基本上避免了现线程不安全问题。
七 线程状态
线程状态在Thread.State枚举类中定义,线程在不同的运行时期,便会进入不同的状态。大多数状态之间都是双向的。线程状态转化如下。
总结
本文针对JAVA中线程模块做了简单的解释与实战,介绍了线程的相关概念,锁的概念,线程间通信的方法等,内容比较简单,但是却比较繁琐,概念性东西比较多,有些内容比较细节,但是却在面试或者使用中是不可忽略的东西。