多线程编程
文章目录
- Java 给多线程编程提供了内置的支持
并发 并行
- 并发:同一时刻多个任务交替执行
- 貌似”同时执行“
- 单核CPU实现多任务就是并发
- 并行:同时间多个任务同时执行
- 多核CPU可以实现并行、并发
进程和线程
进程
应用程序的执行实例,有独立的内存空间和系统资源
-
一个进程包括由操作系统分配的内存空间
- 包含一个或多个线程
-
一个线程不能独立的存在,必须是进程的一部分
-
一个进程一直运行,直到所有的非守护线程都结束运行后才能结束
线程
CPU 调度和分派的基本单位,进程中执行运算的最小单位,可完成一个独立的顺序控制流程
-
一条线程指进程中一个单一顺序的控制流
- 一个进程中可以并发多个线程,每条线程并行执行不同的任务
-
多线程是多任务的一种特别形式,但多线程使用更小的资源开销
-
多线程能编写高效率的程序来达到充分利用 CPU 的目的
-
多线程就是分时利用 CPU,宏观上让所有线程一起执行
- 也叫并发
进程和线程的关系
-
一对多:一个线程只能属于一个进程
- 一个进程可以有多个线程,但至少有一个线程
- 线程是操作系统可识别的最小执行和调度单位
-
资源分配给进程,同一进程的所有线程共享该进程的所有资源
- 同一进程中的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储),但每个线程拥有自己的栈段
- 栈段又叫运行时段,用来存放所有局部变量和临时变量
-
处理机分给线程:真正在处理机上运行的是线程
-
线程在执行过程中,需要协作同步
- 不同进程的线程间要利用消息通信的办法实现同步
使用线程
三种方法
- 实现
Runnable
接口 - 继承
Thread
类本身 - 实现
Callable
和Future
创建线程
Runnable
-
创建线程最简单的方法是实现 Runnable 接口
- 类实现
Runnable
接口- 需要重写
run()
方法public void run();
- 需要使用线程实现的业务逻辑放在
run()
方法里 run()
可以调用其他方法,使用其他类,并声明变量
- 需要重写
- 实例化子类对象,并传入
Thread
类构造参数Thread(Runnable threadOb,String threadName);
threadOb
:实现Runnable
接口的类的实例threadName
:自定义新线程的名字
- 线程创建之后,调用
start()
方法才会运行void start(){};
start()
方法是Thread
类方法- 所以实现
Runnable
接口的子类需要Thread
类代理启动线程
- 所以实现
- 类实现
-
实例
class RunnableDemo implements Runnable { // 实现 Runnable 接口 private Thread t; // 封装线程对象 private String threadName; // 线程名 public RunnableDemo( String name) { // 创建线城市指定线程名 threadName = name; System.out.println("创建线程: " + threadName ); } @Override public void run() { // 重写 run() 方法,实现业务逻辑 System.out.println("运行: " + threadName ); try { for(int i = 2; i > 0; i--) { System.out.println("线程: " + threadName + "-" + i); Thread.sleep(50); // 线程休眠 50ms } }catch (InterruptedException e) { System.out.println("Thread " + threadName + " interrupted."); } } public void start () { // 自定义线程启动方法,调用 Thread 类 start 方法 if (t == null) { t = new Thread (this, threadName); // 通过本类对象实例化 Thread 类对象 t.start (); // 实际启动线程方法 } } } public class TestThread { public static void main(String args[]) { RunnableDemo R1 = new RunnableDemo( "Thread-1" ); // 实例化第一个线程对象 Thread thread = new Thread(R1); // 作为参数创建 Thread 类对象 thread.start(); // 通过 Thread 类启动线程 RunnableDemo R2 = new RunnableDemo( "Thread-2"); // 实例化第二个线程对象 R2.start(); // 通过自定义 start 方法启动线程 /* 实际两种方式完全一样 */ } } /** 创建线程: Thread-1 创建线程: Thread-2 运行: Thread-1 运行: Thread-2 线程: Thread-2-2 线程: Thread-1-2 线程: Thread-2-1 线程: Thread-1-1 */ /* 两个线程同时运行,执行顺序不固定 */
Thread
实现
-
必须重写
run()
方法- 该方法是新线程的入口点
- 必须调用
start()
方法才能执行
-
本质上也是实现
Runnable
接口的一个实例Thread
类实现了Runnable
接口
class ThreadDemo extends Thread {
private String threadName; // 指定线程名
public ThreadDemo( String name) {
threadName = name;
System.out.println("创建 " + threadName );
}
public void run() {
System.out.println("运行线程 " + threadName );
try {
for(int i = 2; i > 0; i--) {
System.out.println("线程: " + threadName + ", " + i);
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("线程 " + threadName + " 退出.");
}
}
public class TestThread {
public static void main(String args[]) {
ThreadDemo T2 = new ThreadDemo( "Thread-2");
T2.start();
}
}
/*
创建 Thread-2
运行线程 Thread-2
线程: Thread-2, 2
线程: Thread-2, 1
线程 Thread-2 退出.
*/
Thread 方法
-
通过 Thread 对象调用
-
常用
-
方法 作用 public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回 public final void setName (String name) 设置线程名称 public final void setPriority (int priority) 更改线程的优先级 public final void setDaemon (boolean on) 将该线程标记为守护线程或用户线程 Public final void join (long millisec) 等待该线程终止的时间最长为 millis 毫秒 public void interrupt() 中断线程 public final boolean isAlive() 测试线程是否处于活动状态
-
-
静态方法
-
方法 描述 public static void yield () 礼让线程,暂停当前正在执行的线程对象,并执行其他线程 public static void sleep (long millisec) 线程休眠(暂停执行)millisec 毫秒,受到系统计时器和调度程序精度和准确性的影响 public static boolean holdsLock (Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true public static Thread currentThread () 返回对当前正在执行的线程对象的引用 public static void dumpStack() 当前线程的堆栈跟踪打印至标准错误流
-
实例
class DisplayMessage implements Runnable { // 实现 Runnable 接口
private String message;
public DisplayMessage(String message) {
this.message = message;
}
@Override
public void run() {
while(true) {
System.out.println(message); // 死循环,线程一直运行
}
}
}
class GuessANumber extends Thread { // 继承 Thread 类
private int number;
public GuessANumber(int number) { // 传入一个数
this.number = number;
}
@Override
public void run() { // 重写 run 方法
int counter = 0;
int guess = 0;
do {
guess = (int) (Math.random() * 100 + 1); // 生成随机数
System.out.println("第" + counter++ + "次;" + this.getName() + " 猜的数为:" + guess);
} while(guess != number); // do-while 循环直到 随机数与参数相同
System.out.println("猜到了!" + this.getName() + " 在第 " + counter + " 次成功");
}
}
public class Test {
public static void main(String args[]) {
Runnable hello = new DisplayMessage("Hello"); // Runnable 实现类
Thread thread1 = new Thread(hello);
thread1.setDaemon(true); // 将此线程设置为守护线程,随其他线程停止而停止
thread1.setName("hello"); // 设置线程名
thread1.start(); // 启动线程
//每一次运行的结果都不一样
Runnable bye = new DisplayMessage("Goodbye"); // Runnable 实现类
Thread thread2 = new Thread(bye);
thread2.setPriority(Thread.MIN_PRIORITY); // 设置线程最小优先级:最后执行
thread2.setDaemon(true); // 设置守护线程
thread2.start(); // 启动线程
//每一次运行的结果都不一样
Thread thread3 = new GuessANumber(27); // Thread 子类,参数 27
thread3.start(); // 启动线程
try {
thread3.join(); // 加入 thread3 优先执行
}catch(InterruptedException e) {
System.out.println("Thread interrupted.");
}
//每一次运行的结果都不一样
Thread thread4 = new GuessANumber(75); // Thread 子类,参数 75
thread4.start();
//每一次运行的结果都不一样
}
}
/*
Hello
Hello //后面仍有可能出现
Goodbye //后面仍有可能出现
Goodbye
......
第46次;Thread-0 猜的数为:27
猜到了!Thread-0 在第 46 次成功 //次数不定,直到数字相等为止
.......
第504次;Thread-1 猜的数为:75 //次数不定,直到数字相等为止
猜到了!Thread-1 在第 504 次成功
Hello
Godbye
......
*/
Callable
实现
- 实现
Callable
接口- 需要重写call()方法
- 给出泛型类型作为
call()
方法返回值类型 call()
方法需要抛出异常
- 使用步骤
- 创建线程类对象:
TestCallable testCallable = new TestCallable();
- 创建执行服务:
ExecutorSeervice ser = Executors.newFixedThreadPool(1);
- 1:线程池大小
- 提交执行:
Future<Boolean> result = ser.submit(testCallable);
TestCallable
:实现接口的线程类对象
- 获取结果:
boolean r = result.get();
- 关闭服务:
ser.shutdownNow();
- 创建线程类对象:
实例
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest(); // 创建子类实例对象
FutureTask<Integer> ft = new FutureTask<>(ctt); // 注册线程服务
for(int i = 0; i < 3; i++) {
System.out.println( Thread.currentThread().getName() + " 的循环变量i的值 " + i ); // 循环获取 main 线程名
if(i == 2) {
new Thread(ft, "有返回值的线程").start(); // 启动线程,设置线程名为:有返回值的线程
}
}
try {
System.out.println("子线程的返回值:"+ ft.get()); // 获取 call 方法返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception{
int i = 0
for( ; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " " + i); // 输出当前线程名和变量 i
}
return i;
}
}
/*
main 的循环变量i的值 0
main 的循环变量i的值 1
main 的循环变量i的值 2
有返回值的线程 0
有返回值的线程 1
有返回值的线程 2
子线程的返回值:3
*/
对比
- 实现
Runnable
、Callable
接口方式创建多线程时,线程类只是实现了Runnable
接口或Callable
接口,还可以继承其他类 - 使用继承
Thread
类的方式创建多线程编写简单- 需要访问当前线程无需使用
Thread.currentThread()
方法,直接使用this
即可
- 需要访问当前线程无需使用
Runnable
和Callable
Runnable
是在 JDK1.0 的时候提出的多线程的实现接口- 而
Callable
是在 JDK 1.5之后提出的
- 而
java.lang.Runnable
接口之中只提供有一个 run()方法,并且没有返回值java.util.concurrent.Callable
接口提供有 call()方法,有返回值
原理
-
调用
start()
后,线程会被放到等待队列,等待 CPU 调度,并不一定马上开始执行,只是将线程置于可动行状态start()
启动线程是因为调用本地的start0()
方法,再根据具体环境分配资源以启动线程
-
通过
JVM
,线程Thread
会调用run()
方法,执行本线程的线程体-
先调用
start
后调用run()
- 为了实现多线程的优点,没 start 不行
-
start()
方法来启动线程,真正实现了多线程运行- 无需等待
run()
方法体执行完毕,可直接继续执行下面的代码 - 通过调用
Thread
类的start()
方法启动一个线程,此线程处于就绪状态,没有运行 - 然后通过此
Thread
类调用方法run()
来完成其运行操作,CPU 再调度其它线程
- 无需等待
-
-
run()
方法当作普通方法直接调用时- 程序是顺序执行:
run()
方法体执行后,才继续执行下面的代码 - 程序中只有主线程, 程序执行路径只有一条,没有达到写线程的目的
- 程序是顺序执行:
run()、start()
- 每个线程都是通过某个特定
Thread
对象所对应的方法run()
来完成操作- 方法
run()
称为线程体,包含要执行的线程的内容,run
方法运行结束, 此线程终止
- 方法
- 通过调用
Thread
类的start()
方法来启动一个线程start()
启动线程是调用本地的start0()
方法,再根据具体环境分配资源以启动线程
线程生命周期
- 线程是一个动态执行的过程,有从产生到死亡的过程
五种状态
- 新建状态:
NEW
new
一个线程对象后,该线程对象就处于新建状态- 保持这个状态直到程序
start()
这个线程
- 就绪状态:
RUNNABLE
- 线程对象调用
start()
方法后,该线程进入就绪状态:READING
- 就绪状态的线程处于就绪队列中,要等待
JVM
里线程调度器的调度
- 线程对象调用
- 运行状态:
RUNNING
- 就绪状态的线程获取 CPU 资源执行
run()
,线程处于运行状态 - 运行状态的线程最为复杂:可以变为阻塞状态、就绪状态和死亡状态
- 就绪状态的线程获取 CPU 资源执行
- 阻塞状态:
BLOCKED
- 线程执行了
sleep
(睡眠)、suspend
(挂起)等方法,失去所占用资源- 该线程从运行状态进入阻塞状态
- 睡眠时间已到或获得设备资源后可以重新进入就绪状态。分为三种:
- 等待阻塞:运行状态中的线程执行
wait()
方法,使线程进入到等待阻塞状态 - 同步阻塞:线程获取
synchronized
同步锁失败- 同步锁被其他线程占用
- 其他阻塞:调用线程的
sleep()
或join()
发出了 I/O 请求时,线程就会进入到阻塞状态sleep()
状态超时,join()
等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态
- 等待阻塞:运行状态中的线程执行
- 线程执行了
- 死亡状态:
TERMINATED
- 运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态(run() 方法结束)
七种状态
-
NEW
:刚创建未启动 -
RUNNABLE
:可运行状态Ready
:就绪状态Runinng
:运行状态
-
BLOCKED
:阻塞状态(等待获取锁) -
WAITING
:等待状态(直到被唤醒) -
TIMED_WITING
:超时等待(有时间限制的等待) -
TERMINATED
:线程终止
线程操作
通知线程
-
B
线程中掌握A
线程的对象,在A
线程中使用变量控制程序运行 -
B
线程中改变该变量以通知A
线程停止 -
public class RunnableDemo extends Thread { static volatile Boolean flag = true; // 控制程序停止的变量 @Override public void run() { System.out.println(this.getName() + " 启动"); while(RunnableDemo.flag){ // 默认死循环 System.out.println(this.getName() + " 在运行"); } System.out.println(this.getName() + " 停止运行"); } } class Test{ public static void main(String[] args) { RunnableDemo runnableDemo = new RunnableDemo(); // 创建线程对象 runnableDemo.setName("主线程"); runnableDemo.start(); // 启动线程 Thread thread = new Thread(() -> { // lambda 表达式实现 Runnable 线程使用 while(true) { for(int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " - " + i); if(i == 2) { RunnableDemo.flag = false; // 判断控制主线程停止 } } } }, "控制线程"); thread.setDaemon(true); // 设置为守护线程 thread.start(); // 启动 } }
线程插队
-
join()
:一旦成功先执行完插队的线程任务再继续原来的线程任务 -
yield()
:线程礼让,但礼让的时间不确定,所以不一定成功 -
public class RunnableDemo extends Thread { static volatile Boolean flag = true; @Override public void run() { System.out.println(this.getName() + " 启动"); while(flag){ for(int i = 0; i < 20; i++) { System.out.println(this.getName() + " " + i); } flag = false; } System.out.println(this.getName() + " 停止运行"); } } class Test{ public static void main(String[] args) { RunnableDemo runnableDemo = new RunnableDemo(); runnableDemo.setName("主线程"); // runnableDemo.start(); Thread thread = new Thread(() -> { for(int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " - " + i); if(i == 5) { try { runnableDemo.start(); // 此时启动主线程 runnableDemo.join(); // 让主线程插队,先执行完毕主险成再继续原来线程任务 // Thread.yield() // 线程礼让,但可能失败,原线程继续执行 } catch( InterruptedException e ) { throw new RuntimeException(e); } } } }, "控制线程"); thread.start(); } }
守护线程
setDaemon(true)
:当其他所有线程结束时,被设为守护线程的线程自动退出
中断线程
interrupted()
:判断线程是否在执行interrupt()
:中断当前执行的线程- 所有正在执行的线程都可以中断
- 中断线程必须处理异常
优先级
-
Java 线程都有优先级、
- 有助于操作系统确定线程的调度顺序
-
优先级是一个整数,取值范围是:[1, 10]
-
1:
Thread.MIN_PRIORITY
-
10:
Thread.MAX_PRIORITY
-
默认线程分配优先级:
NORM_PRIORITY
= 5
-
-
较高优先级的线程对程序更重要,应该在低优先级的线程之前分配处理器资源
-
优先级不能保证线程执行的顺序,而且非常依赖于平台
-
优先级高被调用几率比较大,但是并非绝对先被调用
-
-
getPriority()
:获取当前线程优先级 -
setPriority()
:设置当前线程优先级
线程池
介绍
-
容纳多个线程的容器,其中的线程可以反复使用
- 省去频繁创建线程对象的操作
- 无需反复创建线程而消耗过多资源因
-
如果每个请求到达就创建一个新线程,资源消耗相当大
-
实际使用中创建和销毁线程花费的时间和消耗的系统资源都相当大
- 可能比处理实际的用户请求的时间和资源要多的多
-
活动的线程也需要消耗系统资源
- 一个JVM创建太多的线程,可能使系统过度消耗内存或切换过度,导致系统资源不足
-
为防止资源不足,需要限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数
- 特别是资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务
-
作用
-
主要用来解决线程生命周期开销问题和资源不足问题
-
通过对多个任务重复使用线程,线程创建的开销被分摊到多个任务
-
由于请求到达时线程已存在,消除线程创建带来的延迟,可以立即为请求服务
-
创建
相关API
-
通过线程池工厂创建,调用线程池中的方法获取线程,通过线程去执行任务方法
-
JDK 5.0 提供线程池相关的 API
-
Executors
:工具类,线程池的工厂类;用于创建并返回不同类型的线程池public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象
-
ExecutorService
:真正的线程池接口- 常见子类
ThreadPoolExecutor
void execute(Runnable run)
:执行任务、命令- 无返回值,一般用来执行
Runnable
- 无返回值,一般用来执行
<T> Future <T> submit(Callable<T> task)
:执行任务- 有返回值,一般用来执行
Callable
- 有返回值,一般用来执行
Future<?> submit(Runnable task)
- 执行
Runnable
的有返回值方法
- 执行
void shutdown()
:关闭连接池
- 常见子类
-
Future
接口:用来记录线程任务执行完毕后产生的结果
-
使用
class Test{
public static void main(String[] args){
ExecutorService service = Executors.newFixedThreadPool(10); // 创建服务:创建线程池对象;大小为 10
service.submit(() -> System.out.println(Thread.currentThread().getName())); // lambda 表达式启动 Runnable
service.execute(() -> System.out.println(Thread.currentThread().getName())); // lambda 表达式启动 Runnbale
Integer num = service.submit(() -> {
System.out.println(Thread.currentThread().getName());
return 1;
}).get(); // lambda 表达式启动 Callable,并获取返回值
System.out.println("num = " + num);
service.shutdown(); // 关闭服务
}
}
线程安全
synchronized
-
作用
-
同一进程的多个线程共享同一块存储空间,可能会有访问冲突问题
-
保证数据在被访问时的正确性,在访问中加入锁机制
-
synchronized
:当一个线程拿到对象的排他锁会独占资源,其他线程必须等待,使用后释放锁
-
-
问题
-
一个线程持有锁会导致其他所需要此锁的线程挂起
-
多线程竞争下,加锁、释放锁会导致较多的上下文切换和 调度延时,引起性能问题
-
优先级高的线程等待优先级低的线程释放锁会导致优先级倒置,引起性能问题
-
-
使用
-
synchronized()
:同步(互斥锁)- 同一时间只能有一个对象拿到锁访问,会导致效率降低
- 非静态方法锁默认为:
this
,即本对象- 多个线程间必须为同一个对象
- 静态方法锁默认:
类名.class
- 非静态方法锁默认为:
class A extends Thread{ synchronized (this){} // 同步代码块,得到对象的锁才能操作 public synchronized void m(){ // 同步方法 // 需要同步的方法实现 } public synchronized static void m(){ // 静态同步方法;锁为 A.class // 需要同步的方法实现 } public static void m(){ // 静态方法 synchronized(A.clss) {} // 静态方法中同步代码块,锁为 A.class } }
- 同一时间只能有一个对象拿到锁访问,会导致效率降低
-
-
死锁:多个线程间抢夺资源,各自需要对方手中的锁才能继续执行
- 容易形成死锁:无法释放自身资源,同时无法拿到对方资源
Lock
介绍
-
JDK5
可以通过显式定义同步锁对象,同步锁使用Lock
对象充当Lock
锁需要手动释放JDK1.5
之后出现
-
java.util.concurrent.locks.Lock
接口时控制多个线程对共享资源进行访问的工具- 锁提供对共享资源的独占访问
- 每次只有一个对象对
Lock
对象加锁 - 线程开始前要先获得
Lock
对象
-
java.util.concurrent.locks.ReentrantLock
类实现了Lock
:可重入锁-
Lock lock = new ReentranttLock();
- Reentran:可重用的
-
与
Synchronzied
相同的并发性和内存语义 -
线程安全的控制中,较常用
ReentrantLock
,可以显式加锁、释放锁void lock()
:上锁void unlock()
:释放锁
-
示例
-
步骤
-
创建一个 ReentrantLock 对象
-
可能出现线程安全问题的代码前,调用
Lock
接口中的方法 lock 获取锁对象 -
可能出现线程安全问题的代码后,调用
Lock
接口中的方法unlock
释放锁对象private final ReentrantLock lock = new ReentrantLock(); // 创建锁对象,定义为私有不可改变属性 @Override public void run() { lock.lock(); // 上锁 System.out.println("你好"); // 执行同步方法 lock.unlock(); // 解锁 }
-
-
public class RunnableImpl implements Runnable{ private int ticket = 100; // 定义共享的票源 Lock l = new ReentrantLock(); // 创建 ReentrantLock 对象 @Override public void run() { // 设置线程任务:卖票 while(true){ // 死循环,卖票重复的执行 l.lock(); // lock 方法获取锁对象 if(ticket > 0){ // 判断票是否大于 0 try { Thread.sleep(10); // 提高线程安全问题出现的几率,让程序睡眠 10 毫秒 System.out.println(Thread.currentThread().getName() + "正在卖第 " + ticket + " 张票!"); ticket--; // 票数自减 } catch (InterruptedException e) { e.printStackTrace(); }finally { l.unlock(); // unlock 方法释放锁对象,无论是否异常,都释放锁对象,节约内存,提高程序的效率 } } } } }
synchronzied 和 Lock
-
Lock
:显式锁,手动开关锁synchronzied
:隐式锁,作用域之外自动释放
-
Lock
:只有代码块锁synchronzied
:代码块锁和方法锁
-
Lock
:JVM
花费较少时间调度线程,性能更好- 并且有更好的拓展性,Lock 接口有很多子类
-
Lock
实现提供比使用synchronized
方法和语句更广泛的锁定操作 -
优先使用顺序
- Lock > 同步代码块(已进入方法体,分配资源后)> 同步方法(方法体外)
锁释放
-
释放锁
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步方法、同步代码块遇到
break
、return
- 当前线程的同步方法、同步代码块出现未处理的
Error
或Exception
,异常结束 - 当前线程的同步方法、同步代码块执行了线程对象的
wait()
方法,当前线程暂停并释放锁
-
不会释放锁
- 当前线程的同步方法、同步代码块中调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程执行,不会释放锁 - 当前线程的同步代码块其他线程调用该线程的
suspend()
挂起线程suspend()
和resume()
尽量避免使用,方法已过时
- 当前线程的同步方法、同步代码块中调用
volatile
-
定义在属性上使用
-
正常变量处理流程
- 获取变量原有数据内容副本
- 利用副本进行数学计算
- 将计算后的变量保存在原始空间中
-
加上
volatile
- 不使用副本操作,直接操作原始数据
- 保证数据的可见性,进制指令重排序
- 不保证操作的原子性
ThreadLocal
-
解决静态属性存放数据的线程同步问题
-
多线程情况下静态属性被所有线程对象共享
- 很容易被其他线程将数据覆盖造成资源隐患
-
ThreadLocal
类对线程资源单独存放- 当前线程的资源只允许自己访问
- 同时保存 线程对象 和对应的 数据对象
- 线程对象:自动保存执行时的当前线程对象
- 结构:
Map<Thread,T>
- 使用
Map
保存数据 key
:``Thread.currentThread()`,当前线程对象value
:泛型 T,线程的共享变量
- 使用
-
-
Java 中每个线程对象都有
ThreadLocal
- 常用来解决数据库连接、
Session
管理等
- 常用来解决数据库连接、
-
创建对象:
ThreadLocal<T> threadLocal = new ThreadLocal<T>();
- 执行方法时的当前线程对象为
key
- 执行方法时的当前线程对象为
-
设置数据:
threadLocal.set(T value)
- 许多程序不使用此功能,只依赖于
initialValue()
方法设置线程局部变量值 - 程序中一般重写方法给特定初始值
- 许多程序不使用此功能,只依赖于
-
取出数据:
threadLocal.get()
- 返回此线程局部变量当前线程副本的值
- 第一次调用该方法创建并初始化此副本
-
删除数据:
threadLocal.remove()
- 移除此线程局部变量的值,有助于减少存储需求
- 再次访问此线程局部变量默认将拥有其
initialValue
-
返回初始值:
protected T initialValue() {return null;}
- 返回此线程局部变量当前线程的初始值
- 最多在每次访问线程获得线程局部变量时调用一次
- 即 第一次使用
get()
方法访问变量时 - 若线程先调用
set()
方法则不会载调用此方法
- 即 第一次使用
- 该方法只返回
null
,若要将线程局部变量初始化为其他值- 为
ThreadLocal
创建子类重写该方法 - 通常使用匿名内部类
- 调用适当构造方法返回新构造对象
- 为
-
class Channel { // 封装为私有静态全局常量,ThreadLocal 类按线程分别存放数据,只有对应线程才可以获得自己的资源 private static final ThreadLocal<Message> THREAD_LOCAL = new ThreadLocal<>(); // 设置消息 public static void setMessage(Message mess) { THREAD_LOCAL.set(mess); // 存放在 ThreadLocal 中 } public static void send() { // 发送消息 // 从 ThreadLocal 实例对象中获取当前线程对应的资源 System.out.println(Thread.currentThread().getName() + "发送消息: " + THREAD_LOCAL.get().getInfo()); } } class Message { // 要发送的消息体 private String info; public Message(String info){ this.info = info;} public String getInfo() {return info;} } public class Test{ public static void main(String[] args) { // 启动多个线程同时操作消息 new Thread(() -> { Channel.setMessage(new Message("废物")); // 发送消息 Channel.send(); }, "线程A").start(); new Thread(() -> { Channel.setMessage(new Message("菜狗")); Channel.send(); }, "线程B").start(); new Thread(() -> { Channel.setMessage(new Message("小点心")); Channel.send(); }, "线程C").start(); } } /* 运行结果: 按线程自动抢占资源的快慢执行顺序有所不同,但消息内容各自保存不会被覆写 线程C发送消息: 小点心 线程B发送消息: 菜狗 线程A发送消息: 废物 */
线程通信
Object类的线程方法
-
notify()
:随机唤醒一个该对象上等待的线程- 返回的前提是该线程获取到了对象的锁
-
notifyAll()
:唤醒所有等待在该对象上的线程 -
wait()
:调用该方法的线程进入WAITING
状态- 被该对象的其他线程的唤醒或线程中断才会返回
- 调用
wait()
方法后,会释放对象的锁
-
wait(long timeout)
:超时等待一段时间- 参数:毫秒,即等待长达
timeout
毫秒,如果没有通知就超时返回
- 参数:毫秒,即等待长达
-
wait(long timeout, int nanos)
:超时等待一段时间后可以额外等待更精确的时间- 对于超时时间更细粒度的控制,可以达到纳秒
timeout
:超市等待,单位:毫秒nanos:
额外等待,单位:纳秒