要了解多线程,先要知道CPU时间片轮转机制
我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。
时间片轮转调度是一种算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
CPU时间片轮转机制原理解释如下:
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结来,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾
时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。CPU时间的20%被浪费在了管理开销上了。
为了提高CPU效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s才获得运行机会。多数用户无法忍受一条简短命令要5才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发
结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。
1.什么是线程
线程就是进程中运行的多个子任务,是操作系统调用的最小单元
2.线程的状态
New:新建状态,new出来,还没有调用start
Runnable(包括两个状态 READY RUNNING):可运行状态,调用start进入可运行状态,处于可运行状态的线程正在jvm中执行,可能运行也可能没有运行,取决于操作系统的调度
Blocked:阻塞状态,被锁阻塞,暂时不活动,等待获取synchronized锁的状态 ,只有等待synchronized的状态才是Blocked状态
,那么假如是等待获取ReentrantLock锁的时候,是Blocked状态吗?不是的,因为ReentrantLock底层实现也是通过LockSupport实现线程等待的,所以等待获取显示锁的状态是Waiting或者Timed Waiting
Waiting:等待状态,不活动,不运行任何代码,等待线程调度器调度,会使线程进入此状态的方法有:
1.object.wait()
2.Thread.join()
3.LockSupport.park()
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断),如:
object.wait(),等待其他线程Object.notify() or Object.notifyAll()
Thread.join(),等待其他线程terminate
Timed Waiting:超时等待,该状态不同于waiting,他可以在指定时间自行返回,会使线程进入此状态的方法有:
1.Thread.sleep(指定休眠时间)
2.Object.wait(指定等待时间)
3.Thread.join(指定时间)
4.LockSupport.parkNanos(指定时间)
5.LockSupport.parkUntil(指定时间)
Terminated:终止状态,包括正常终止和异常终止
如下代码,中第一个线程是Timed Waiting状态,第二个线程等待获取锁,是Blocked状态
public static void main(String[] args) {
Object o = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (o) {
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
synchronized (o) {
System.out.println("等待获取锁");
}
}
}).start();
}
2.线程的创建
a.继承Thread重写run方法
b.实现Runnable重写run方法
c.实现Callable重写call方法
实现Callable和实现Runnable类似,但是功能更强大,具体表现在
a.可以在任务结束后提供一个返回值,Runnable不行
b.call方法可以抛出异常,Runnable的run方法不行
c.可以通过运行Callable得到的Fulture对象监听目标线程调用call方法的结果,得到返回值,(fulture.get(),调用后会阻塞,直到获取到返回值)
3.线程中断
一般情况下,线程不执行完任务不会退出,但是在有些场景下,我们需要手动控制线程中断结束任务,Java中有提供线程中断机制相关的Api,每个线程都一个状态位用于标识当前线程对象是否是中断状态
stop() 废弃方法,开发中不要使用。因为一调用,线程就立刻停止,此时有可能引发相应的线程安全性问题
Thread.stop()
从SUN的官方文档可以得知,调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:
- 即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。
- 释放该线程所持有的所有的锁。调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。
public class TestThreadStop extends Thread{
public static void main(String[] args) throws InterruptedException {
TestThreadStop thread = new TestThreadStop();
thread.start();
thread.print();
//休眠1s后stop线程,暴力停止,这个时候由于i++之后还没有执行到j++
//线程就直接在这里退出了,导致j还是原始值0
Thread.sleep(1000);
thread.stop();
}
int i=0;
int j=0;
@Override
public void run() {
super.run();
i++;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
j++;
}
private void print() {
System.out.println("i="+i);
System.out.println("j="+j);
}
}
Thread.interrupt方法
public boolean isInterrupted() //判断中断标识位是否是true,不会改变标识位
public void interrupt() //将中断标识位设置为true
public static boolean interrupted() //判断当前线程是否被中断,并且该方法调用结束的时候会清空中断标识位,也就是说调用之后interrupt都会被置为false
需要注意的是interrupt()方法并不会真的中断线程,它只是将中断标识位设置为true,具体是否要中断由程序来判断,如下,只要线程中断标识位为false,也就是没有中断就一直执行线程方法
new Thread(new Runnable(){
while(!Thread.currentThread().isInterrupted()){
//执行线程方法
}
}).start();
前边我们提到了线程的六种状态,New Runnable Blocked Waiting Timed Waiting Terminated,那么在这六种状态下调用线程中断的代码会怎样呢,New和Terminated状态下,线程不会理会线程中断的请求,既不会设置标记位,在Runnable和Blocked状态下调用interrupt会将标志位设置位true,在Waiting和Timed Waiting状态下会发生InterruptedException异常,针对这个异常我们如何处理?
1.在catch语句中通过interrupt设置中断状态,因为发生中断异常时,中断标志位会被复位(被设置为false),我们需要重新将中断标志位设置为true,这样外界可以通过这个状态判断是否需要中断线程
new Thread(new Runnable(){
while(!Thread.currentThread().isInterrupted()){
//执行线程方法
//sleep状态下调用interrupt,会抛出InterruptedException ,同时异常发生
//后,Thread.currentThread().isInterrupted() 值变为false,也就是被复位,
// 导致线程无法退出,所以需要在发生异常时主动将异常标志设置为true
//Thread.currentThread().interrupt();
try{
Thread.sleep(2000);
....
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
}).start();
2.更好的做法是,不捕获异常,直接抛出给调用者处理,这样更灵活
注意
中断标志位其实也就是定义了一个变量来标识是否要退出线程,那么和我们自己定义一个有什么区别呢?有,而且区别很大,不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志
。这种情况下,使用中断会更好,因为,一、一般的阻塞方法,如sleep等本身就支持中断的检查,二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
4.线程的挂起与恢复
什么是挂起线程?
线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。在线程挂起后,可以通过重新唤醒线程来使之恢复运行
为什么要挂起线程?
cpu分配的时间片非常短、同时也非常珍贵。避免资源的浪费。
如何挂起线程?
被废弃的方法
thread.suspend() 该方法不会释放线程所占用的资源。如果使用该方法将某个线程挂起,则可能会使其他等待资源的线程死锁
thread.resume() 方法本身并无问题,但是不能独立于suspend()方法存在
public class TestThread implements Runnable{
//注意使用同一把锁
static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new TestThread(),"线程1");
thread1.start();
Thread.sleep(2000);
Thread thread2 = new Thread(new TestThread(),"线程2");
//为什么suspend会导致死锁,因为调用suspend挂起线程不会释放锁,如果挂起后meiyou
//及时resume,就会导致锁一直被持有,从而导致死锁
thread2.start();
//开启线程之后立即调用了resume,为什么没有释放掉呢,因为程序执行的非常快,thread2.start()
//开启线程,是相对比较耗时的操作,那么可能出现在线程suspend之前,resume就先一步执行了
Thread.sleep(2000);
//直到线程1resume之后,线程二才获取到了锁
thread1.resume();
}
@Override
public void run() {
synchronized (o) {
System.out.println(Thread.currentThread().getName()+"挂起线程");
Thread.currentThread().suspend();
}
System.out.println(Thread.currentThread().getName()+"释放线程");
}
}
可以使用的方法
wait() 暂停执行、放弃已经获得的锁、进入等待状态
notify() 随机唤醒一个在等待锁的线程
notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源
使用这种方式就不会产生那种问题,因为wait后释放了锁
public class TestThread implements Runnable{
//注意使用同一把锁
static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new TestThread(),"线程1");
thread1.start();
Thread.sleep(2000);
Thread thread2 = new Thread(new TestThread(),"线程2");
//为什么suspend会导致死锁,因为调用suspend挂起线程不会释放锁,如果挂起后meiyou
//及时resume,就会导致锁一直被持有,从而导致死锁
thread2.start();
//开启线程之后立即调用了resume,为什么没有释放掉呢,因为程序执行的非常快,thread2.start()
//开启线程,是相对比较耗时的操作,那么可能出现在线程suspend之前,resume就先一步执行了
}
@Override
public void run() {
synchronized (o) {
System.out.println(Thread.currentThread().getName()+"挂起线程");
try {
o.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"释放线程");
}
}
什么时候适合使用挂起线程?
等待某些未就绪的资源,直到notify方法被调用
5.线程的优先级
线程的优先级告诉程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,程序会尽可能地先运行优先级的那个线程。 但是,这并不表示优先级较低的线程不会运行。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。
线程的优先级设置可以为1-10的任一数值,Thread类中定义了三个线程优先级,分别是:MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情况下推荐使用这几个常量,不要自行设置数值。
不同平台,对线程的优先级的支持不同。编程的时候,不要过度依赖线程优先级,如果你的程序运行是否正确取决于你设置的优先级是否按所设置的优先级运行,那这样的程序不正确
任务:
快速处理:设置高的优先级
慢慢处理:设置低的优先级
public class TestThreadStop extends Thread{
public static void main(String[] args) throws InterruptedException {
TestThreadStop thread1 = new TestThreadStop();
thread1.setName("线程1");
thread1.start();
thread1.setPriority(Thread.MAX_PRIORITY);
TestThreadStop thread2 = new TestThreadStop();
thread2.setName("线程2");
thread2.start();
thread2.setPriority(Thread.MIN_PRIORITY);
}
int i=0;
@Override
public void run(){
super.run();
while(true) {
//理论上应该优先级高的线程打印的更多,但是不一定
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(20);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
守护线程
线程分类:用户线程、守护线程
守护线程:任何一个守护线程都是整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。当JVM实例中最后一个非守护线程结束时,也随JVM一起退出
守护线程的使用举例:jvm垃圾清理线程
建议: 尽量少使用守护线程,因其不可控
不要在守护线程里去进行读写操作、执行计算逻辑
public class TestThreadStop extends Thread{
public static void main(String[] args) throws InterruptedException {
TestThreadStop thread1 = new TestThreadStop();
thread1.setName("线程1");
//设置thread1为主线程的守护线程
thread1.setDaemon(true);
thread1.start();
//主线程休眠两秒后执行完成,那么守护线程也应该退出
Thread.sleep(2000);
}
int i=0;
@Override
public void run(){
super.run();
while(true) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
并发编程
线程安全
当多个线程访问某个类,当运行时环境不管采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的 ---《并发编程实战》
串行与并行的区别:
1.串行:洗茶具、打水、烧水、等水开、冲茶
2.并行:打水、烧水同时洗茶具、水开、冲茶
3.好处:可以缩短整个流程的时间
并发编程目的:
1.摩尔定律:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。这一定律揭示了信息技术进步的速度。
2.让程序充分利用计算机资源
3.加快程序响应速度(耗时任务、web服务器)
4.简化异步事件的处理
什么时候适合使用并发编程:
1.任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况
2.任务执行时间过长,可以划分为分工明确的子任务:比如分段下载
3.任务间断性执行:日志打印
4.任务本身需要协作执行:比如生产者消费者问题
从字节码角度剖析为什么自增操作不是线程安全的
如何得到一个类的字节码文件?
javac -encoding UTF-8 UnsafeThread.java 编译成.class
javap -c UnsafeThread.class 进行反编译,得到相应的字节码指令
对如下一段代码
public static int i = 0;
public void increase(){
i++;
}
i++这一行对应的字节码文件如下
0: getstatic #2 获取指定类的静态域,并将其压入栈顶
3: iconst_1 将int型1压入栈顶
4: iadd 将栈顶两个int型相加,将结果压入栈顶
5: putstatic #2 为指定类静态域赋值
8: return
产生线程不安全问题的原因:
num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快速切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性问题。
并发编程的挑战之频繁的上下文切换
1.cpu为线程分配时间片,时间片非常短(毫秒级别),cpu不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这 个任务的状态,让我们感觉是多个程序同时运行的,上下文的频繁切换,会带来一定的性能开销
如何减少上下文切换的开销?
a.无锁并发编程
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
b.CAS
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
c.使用最少线程。
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
d.协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间
的切换。(基本不使用)
并发编程的挑战之死锁
如何查看死锁,可以通过jdk工具,在控制台输入jconsole,弹出界面后连接,在线程选项卡中点击检测死锁,既可以看到
package com.xdclass.synopsis;
/**
* 死锁Demo
*/
public class DeadLockDemo {
private static final Object HAIR_A = new Object();
private static final Object HAIR_B = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (HAIR_A) {
try {
Thread.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (HAIR_B) {
System.out.println("A成功的抓住B的头发");
}
}
}).start();
new Thread(()->{
synchronized (HAIR_B) {
synchronized (HAIR_A) {
System.out.println("B成功抓到A的头发");
}
}
}).start();
}
}
并发编程的挑战之线程安全
import java.util.concurrent.CountDownLatch;
/**
* 线程不安全操作代码实例
*/
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 每次调用对num进行++操作
*/
public static void inCreate() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countdownLatch
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) {
System.out.println(num);
break;
}
}
}
}
并发编程的挑战之资源限制
硬件资源
服务器: 1m
本机:2m
带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。
软件资源
数据库连接 500个连接 1000个线程查询 并不会因此而加快
socket
单例与线程安全
public class SingleTon {
//1.volatile禁止指令重排序
private static volatile SingleTon instance = null;
private SingleTon() {}
public static SingleTon getInstance() {
//2.创建之后再次获取可以避免锁
if(instance == null) {
synchronized(SingleTon.class) {
if(instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
public static void main(String[] args) {
for(int i = 0 ;i<20;i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(SingleTon.getInstance());
}
}).start();
}
}
}
4.重入锁与条件对象,同步方法和同步代码块
。。。
5.volatile关键字
volatile为实例域的同步访问提供了免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就直到该域可能被另一个线程并发更新
Android中的线程有那些,原理与各自特点
AsyncTask,HandlerThread,IntentService
AsyncTask原理:内部是Handler和两个线程池实现的,Handler用于将线程切换到主线程,两个线程池一个用于任务的排队,一个用于执行任务,当AsyncTask执行execute方法时会封装出一个FutureTask对象,将这个对象加入队列中,如果此时没有正在执行的任务,就执行它,执行完成之后继续执行队列中下一个任务,执行完成通过Handler将事件发送到主线程。AsyncTask必须在主线程初始化,因为内部的Handler是一个静态对象,在AsyncTask类加载的时候他就已经被初始化了。在Android3.0开始,execute方法串行执行任务的,一个一个来,3.0之前是并行执行的。如果要在3.0上执行并行任务,可以调用executeOnExecutor方法
HandlerThread原理:继承自Thread,start开启线程后,会在其run方法中会通过Looper创建消息队列并开启消息循环,这个消息队列运行在子线程中,所以可以将HandlerThread中的Looper实例传递给一个Handler,从而保证这个Handler的handleMessage方法运行在子线程中,Android中使用HandlerThread的一个场景就是IntentService
IntentService原理:继承自Service,它的内部封装了HandlerThread和Handler,可以执行耗时任务,同时因为它是一个服务,优先级比普通线程高很多,所以更适合执行一些高优先级的后台任务,HandlerThread底层通过Looper消息队列实现的,所以它是顺序的执行每一个任务。可以通过Intent的方式开启IntentService,IntentService通过handler将每一个intent加入HandlerThread子线程中的消息队列,通过looper按顺序一个个的取出并执行,执行完成后自动结束自己,不需要开发者手动关闭
什么是线程安全性
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。----《并发编程实战》
什么是线程不安全?
多线程并发访问时,得不到正确的结果。
产生线程安全性的原因:
1.多线程环境
2.多线程操作同一共享资源
3.对该共享资源进行了非原子性操作
避免产生线程安全性
打破3点成因中的任一点
1.多线程环境--将多线程改成单线程,必要的代码加锁访问
2.多个线程操作同一共享资源--不再共享资源(ThreadLocal 不共享 无状态化 不可变)
3.3.对该共享资源进行了非原子性操作--将非原子性操作变成原子性操作(加锁,使用JDK自带的原子性操作的类AtomicInteger,JUC提供的相应的并发工具类)
原子性 可见性 有序性
原子性:
什么是原子性操作?一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。对基本数据类型的读取和赋值操作是原子性操作,这些操作不可被中断,是一步到位的,例如x=3是原子性操作,而y = x就不是,它包含两步:第一读取x,第二将x写入工作内存;x++也不是原子性操作,它包含三部,第一,读取x,第二,对x加1,第三,写入内存。如何把非原子性操作变成原子性,volatile关键字仅仅保证可见性,并不保证原子性,synchronize关键字。
使得操作具有原子性原子性操作的类如:AtomicInteger AtomicBoolean AtomicLong AtomicReference
可见性:
指线程之间的可见性,既一个线程修改的状态对另一个线程是可见的。volatile修饰可以保证可见性,它会保证修改的值会立即被更新到主存,所以对其他线程是可见的,普通的共享变量不能保证可见性,因为被修改后不会立即写入主存,何时被写入主存是不确定的,所以其他线程去读取的时候可能读到的还是旧值
有序性:
Java中的指令重排序(包括编译器重排序和运行期重排序)可以起到优化代码的作用,但是在多线程中会影响到并发执行的正确性,使用volatile可以保证有序性,禁止指令重排
volatile可以保证可见性 有序性,但是无法保证原子性,在某些情况下可以提供优于锁的性能和伸缩性,替代sychronized关键字简化代码,但是要严格遵循使用条件。
线程池ThreadPoolExecutor
为什么要用线程池?
a.降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
b.提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
c.提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险**
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
线程池的工作原理:线程池可以减少创建和销毁线程的次数,从而减少系统资源的消耗,当一个任务提交到线程池时
a. 首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线程执行任务,否则进入下一步
b. 判断工作队列是否已满,没有满则加入工作队列,否则执行下一步
c. 判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否则执行饱和策略,默认抛出异常
线程池的种类
1.FixedThreadPool:可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队
2.SingleThreadExecutor:单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,如果没有运行线程,则创建一个线程执行,如果正在运行则加入队列等待,可以保证所有任务在一个线程中按照顺序执行,和FixedThreadPool的区别只有数量
3.CachedThreadPool:按需创建的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE个,每次提交
任务如果有空闲线程则由空闲线程执行,没有空闲线程则创建新的线程执行,适用于大量的需要立即处理的并且耗时较短的任务
4.ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,用于延时执行任务或定期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE
线程同步机制与原理,举例说明
为什么需要线程同步?当多个线程操作同一个变量的时候,存在这个变量何时对另一个线程可见的问题,也就是可见性。每一个线程都持有主存中变量的一个副本,当他更新这个变量时,首先更新的是自己线程中副本的变量值,然后会将这个值更新到主存中,但是是否立即更新以及更新到主存的时机是不确定的,这就导致当另一个线程操作这个变量的时候,他从主存中读取的这个变量还是旧的值,导致两个线程不同步的问题。线程同步就是为了保证多线程操作的可见性和原子性,比如我们用synchronized关键字包裹一端代码,我们希望这段代码执行完成后,对另一个线程立即可见,另一个线程再次操作的时候得到的是上一个线程更新之后的内容,还有就是保证这段代码的原子性,这段代码可能涉及到了好几部操作,我们希望这好几步的操作一次完成不会被中间打断,锁的同步机制就可以实现这一点。一般说的synchronized用来做多线程同步功能,其实synchronized只是提供多线程互斥,而对象的wait()和notify()方法才提供线程的同步功能。JVM通过Monitor对象实现线程同步,当多个线程同时请求synchronized方法或块时,monitor会设置几个虚拟逻辑数据结构来管理这些多线程。新请求的线程会首先被加入到线程排队队列中,线程阻塞,当某个拥有锁的线程unlock之后,则排队队列里的线程竞争上岗(synchronized是不公平竞争锁,下面还会讲到)。如果运行的线程调用对象的wait()后就释放锁并进入wait线程集合那边,当调用对象的notify()或notifyall()后,wait线程就到排队那边。这是大致的逻辑。
为什么HashMap线程不安全(hash碰撞与扩容导致)
HashMap的底层存储结构是一个Entry数组,每个Entry又是一个单链表,一旦发生Hash冲突的的时候,HashMap采用拉链法解决碰撞冲突,因为hashMap的put方法不是同步的,所以他的扩容方法也不是同步的,在扩容过程中,会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。当多个线程同时检测到hashmap需要扩容的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。扩容的时候 可能会引发链表形成环状结构
进程线程的区别
1.地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
2.资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
3.一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
4.进程切换时,消耗的资源大,效率不高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
5.执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
6.线程是处理器调度的基本单位,但是进程不是。
7.两者均可并发执行。
死锁的产生条件,如何避免死锁
死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个线程使用
2.请求与保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放。
3.不可剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能 由获得该资源的线程自己来释放(只能是主动释放)。
4.循环等待条件: 若干线程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
避免死锁的方法:系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。
在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。 一般来说互斥条件是无法破坏的,所以在预防死锁时主要从其他三个方面入手 :
(1)破坏请求和保持条件:在系统中不允许进程在已获得某种资源的情况下,申请其他资源,即要想出一个办法,阻止进程在持有资源的同时申请其它资源。
方法一:在所有进程开始运行之前,必须一次性的申请其在整个运行过程中所需的全部资源,
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源
(2)破坏不可抢占条件:允许对资源实行抢夺。
方式一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方式二:如果一个进程请求当前被另一个进程占有的资源,则操作系统可以抢占另一个进程,要求它释放资源,只有在任意两个进程的优先级都不相同的条件下,该方法才能预防死锁。
(3)破坏循环等待条件
对系统所有资源进行线性排序并赋予不同的序号,这样我们便可以规定进程在申请资源时必须按照序号递增的顺序进行资源的申请,当以后要申请时需检查要申请的资源的编号大于当前编号时,才能进行申请。
利用银行家算法避免死锁:
所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。
按照银行家算法的思想,当进程请求资源时,系统将按如下原则分配系统资源:
synchronized
对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
内置锁:
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁:
内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
当synchronized关键字用在不同地方时的含义
a.修饰非静态方法:锁住对象的实例,使用同一个对象的锁时才有互斥效果,加入一个类创建了两个对象,那么这两个对象的锁是没有互斥关系的
b.修饰静态方法:锁住整个类。因为是锁住的类,那么不管这个类创建多少个对象,这些对象调用这个同步方法的时候都存在互斥关系,会等待一个执行完成在执行另一个
c.修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容,传哪个对象就锁哪个
synchronized 关键字的底层原理
指在汇编指令层面,先编译成class,再进行反编译得到
同步代码块
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
同步方法
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
JDK1.6 对锁的实现引入了大量的优化,如
偏向锁、
轻量级锁、
自旋锁:通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)
适应性自旋锁:自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。
锁消除、
锁粗化等技术来减少锁操作的开销。
锁的分类
1.自旋锁: 线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起
2.阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态
3.重入锁:支持线程再次进入的锁,就跟我们有房间钥匙,可以多次进入房间类似
4.读写锁: 两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享
5.互斥锁: 上厕所,进门之后就把门关了,不让其他人进来
6.悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
7.乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
8.公平锁:大家都老老实实排队,对大家而言都很公平
9.非公平锁:一部分人排着队,但是新来的可能插队
10.偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
11.独占锁:独占锁模式下,每次只能有一个线程能持有锁
12.共享锁:允许多个线程同时获取锁,并发访问共享资源
synchronized和lock的区别
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
说说 synchronized 关键字和 volatile 关键字的区别
a.volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
b.多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
c.volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
d.volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
ReentrantLock 之公平锁和非公平锁
在ReentrantLock中包含了公平锁和非公平锁两种锁,通过查看源码可以看到这两种锁都是继承自Sync,而Sync又继承自AbstractQueuedSynchronizer,在AbstractQueuedSynchronizer中通过CLH队列实现了多线程锁的排队使用,但是该队列的实现并不能保证锁的公平竞争,但是在某些业务场景中会需要保证先到的线程先得到锁,所以就有了公平锁和非公平锁的诞生。通过分析ReentrantLock中的公平锁和非公平锁的实现,其中tryAcquire是公平锁和非公平锁实现的区别,可以看出在公平锁中,每一次的tryAcquire都会检查CLH队列中是否仍有前驱的元素,如果有那么继续等待,通过这种方式来保证先来先服务的原则;而非公平锁,首先是检查并设置锁的状态,这种方式会出现即使队列中有等待的线程,但是新的线程仍然会与排队线程中的对头线程竞争(但是排队的线程是先来先服务的),所以新的线程可能会抢占已经在排队的线程的锁,这样就无法保证先来先服务,但是已经等待的线程们是仍然保证先来先服务的,所以总结一下公平锁和非公平锁的区别:
1、公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
2、非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
读写锁特性及ReentrantReadWriteLock的使用
场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
特性:写写互斥、读写互斥、读读共享
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
从下边的代码和两次打印的log中可以看到,在家读写锁之前,线程执行是很混乱的,一个线程在写入新值的同时,另一个线程也可以去写入,一个线程写入完成之前,其他线程也可以去读区这个值,这就导致程序运行不符合我们的设计,
加了读写锁之后从log中可以看出,一个线程在写的时候,其他线程不能写入,并且不能读取,但是多个线程可以同时进行读取,这就是写写互斥、读写互斥、读读共享,一次运行情况也许说明不了问题,但是可以进行多次尝试验证
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestReentrantReadWriteLock {
int i;
int j;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 读方法执行");
System.out.println(Thread.currentThread().getName()+" i="+i+",j="+j);
System.out.println(Thread.currentThread().getName()+" 读方法执行完成");
}finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 写方法执行");
i++;
System.out.println(Thread.currentThread().getName()+" i=="+i);
try {
Thread.sleep(2000);
}catch (Exception e){
}
j++;
System.out.println(Thread.currentThread().getName()+" j=="+j);
System.out.println(Thread.currentThread().getName()+" 写方法执行完成");
}finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
TestReentrantReadWriteLock demo = new TestReentrantReadWriteLock();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.write();
demo.read();
}
}).start();
}
}
}
加读写锁之前的打印
Thread-0 写方法执行
Thread-2 写方法执行
Thread-1 写方法执行
Thread-2 i==2
Thread-0 i==1
Thread-1 i==3
Thread-2 j==1
Thread-1 j==3
Thread-1 写方法执行完成
Thread-0 j==2
Thread-0 写方法执行完成
Thread-0 读方法执行
Thread-1 读方法执行
Thread-2 写方法执行完成
Thread-2 读方法执行
Thread-1 i=3,j=3
Thread-0 i=3,j=3
Thread-0 读方法执行完成
Thread-1 读方法执行完成
Thread-2 i=3,j=3
Thread-2 读方法执行完成
Process finished with exit code 0
加读写锁之后的打印
Thread-0 写方法执行
Thread-0 i==1
Thread-0 j==1
Thread-0 写方法执行完成
Thread-1 写方法执行
Thread-1 i==2
Thread-1 j==2
Thread-1 写方法执行完成
Thread-2 写方法执行
Thread-2 i==3
Thread-2 j==3
Thread-2 写方法执行完成
Thread-2 读方法执行
Thread-2 i=3,j=3
Thread-2 读方法执行完成
Thread-0 读方法执行
Thread-1 读方法执行
Thread-0 i=3,j=3
Thread-0 读方法执行完成
Thread-1 i=3,j=3
Thread-1 读方法执行完成
Process finished with exit code 0
ReentrantReadWriteLock实现缓存系统
这一功能使用普通的锁或者直接使用synchronized就可以实现相同的功能,为什么还要这么麻烦的使用读写锁呢,最简单的原因就是为了提高效率,类似下边的场景,可以当作一个两级缓存的模型,首先从内存Hashmap中读取,如果内存中不存在,再去数据库读取并写入内存,这种情况下,我们可想而知,从内存读取之后只要不被销毁,几乎就不再需要从数据库读取并写入内存的操作了,所以读相对于写的频率是高出很多的。
假设我们直接使用synchronized关键字,实现起来很简单,但是当内存中存在缓存的情况下,每次读取一次完成之前,其他线程无法进入,就会浪费很多效率而读写锁的特性是读读共享的,所有线程可以同时进入读取,效率会有很大的提高
package pipe;
import java.util.HashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class CachedData {
private HashMap cache = new HashMap();
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
CachedData cache = new CachedData();
for(int i = 0;i< 3;i++) {
new Thread(new Runnable() {
@Override
public void run() {
String value = cache.getCache("renzhenming");
System.out.println("---------------value="+value);
}
}).start();
}
}
/**
* 这一功能使用普通的锁或者直接使用synchronized就可以实现相同的功能,为什么还要这么麻烦的
* 使用读写锁呢,最简单的原因就是为了提高效率,类似下边的场景,可以当作一个两级缓存的模型,
* 首先从内存Hashmap中读取,如果内存中不存在,再去数据库读取并写入内存,这种情况下,我们可想
* 而知,从内存读取之后只要不被销毁,几乎就不再需要从数据库读取并写入内存的操作了,所以读相对
* 于写的频率是高出很多的。
* 假设我们直接使用synchronized关键字,实现起来很简单,但是当内存中存在缓存的情况下,每次读取
* 一次完成之前,其他线程无法进入,就会浪费很多效率
* 而读写锁的特性是读读共享的,所有线程可以同时进入读取,效率会有很大的提高
* @param key
* @return
*/
public String getCache(String key) {
rwl.readLock().lock();
System.out.println(Thread.currentThread().getName()+" 获取到读锁");
//模拟耗时,验证读读共享
sleep();
String value = null;
try {
value = cache.get(key);
if (value == null || "".equals(value)) {
System.out.println(Thread.currentThread().getName()+"--1-- value无值,从数据库读取");
// 在尝试获取写锁之前必须释放读锁(不支持锁升级)
rwl.readLock().unlock();
System.out.println(Thread.currentThread().getName()+" 释放了读锁");
sleep();
rwl.writeLock().lock();
System.out.println(Thread.currentThread().getName()+" 获取到写锁");
sleep();
try {
//获取到写锁之后,必须再次检测缓存状态,因为在本线程获取到写锁之前可能已经
//有另一个线程获取过写锁并更新了value的值,和单利模式的双层检测是相同道理
value = cache.get(key);
if (value == null || "".equals(value)) {
System.out.println(Thread.currentThread().getName()+"--2-- value无值,从数据库读取");
sleep();
value = "模拟读取到数据库中的数据";
cache.put(key, value);
}else {
System.out.println(Thread.currentThread().getName()+"--2-- value已经有值,返回");
}
//释放写锁之前将锁降级为读锁
rwl.readLock().lock();
System.out.println(Thread.currentThread().getName()+" 获取到读锁");
sleep();
}catch (Exception e) {
e.printStackTrace();
}finally {
//释放写锁,仍然持有读锁
rwl.writeLock().unlock();
System.out.println(Thread.currentThread().getName()+" 释放了写锁");
sleep();
}
}else {
System.out.println(Thread.currentThread().getName()+"--1-- value有值,返回");
}
}catch(Exception e) {
e.printStackTrace();
}finally {
rwl.readLock().unlock();
System.out.println(Thread.currentThread().getName()+" 释放了读锁");
sleep();
}
return value;
}
public void sleep() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
源码探秘之AQS如何用单一int值表示读写两种状态
int 是32位,将其拆分成两个无符号short
高位表示读锁 低位表示写锁
0000000000000000 0000000000000000
两种锁的最大可重入次数均为65535也即是2的16次方减去1
读锁: 每次都从当前的状态加上65536
0000000000000000 0000000000000000
0000000000000001 0000000000000000
-----------------------------------
0000000000000001 0000000000000000
0000000000000001 0000000000000000
-----------------------------------
0000000000000010 0000000000000000
获取读锁个数,将state整个无符号右移16位就可得出读锁的个数
0000000000000001
写锁:每次都直接加1
0000000000000000 0000000000000000
0000000000000000 0000000000000001
-----------------------------------
0000000000000000 0000000000000001
获取写锁的个数
0000000000000000 0000000000000001
0000000000000000 1111111111111111
-----------------------------------
0000000000000000 0000000000000001
锁降级
写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
应用场景
用于对数据比较敏感,需要修改数据后获取到修改后数据的值,并进行其他操作的场景
代码模拟锁降级应用场景
1.写入数据后再读,不加锁的情况
public class TestLockDegrade {
private int i;
public void doSomething() {
//写值
i++;
//模拟其他耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//读值
if(i == 1) {
System.out.println("i的值为1");
}else {
System.out.println("i的值为"+i);
}
}
public static void main(String[] args) {
TestLockDegrade lock = new TestLockDegrade();
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lock.doSomething();
}
}).start();
}
}
}
打印结果:
i的值为4
i的值为4
i的值为4
i的值为4
2.对写入数据和读区数据的代码分别加写锁和读锁,结果还是一样
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class TestLockDegrade {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReadLock readLock = lock.readLock();
WriteLock writeLock = lock.writeLock();
private int i;
public void doSomething() {
writeLock.lock();
//写值
i++;
writeLock.unlock();
//模拟其他耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
readLock.lock();
//读值
if(i == 1) {
System.out.println("i的值为1");
}else {
System.out.println("i的值为"+i);
}
readLock.unlock();
}
public static void main(String[] args) {
TestLockDegrade lock = new TestLockDegrade();
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lock.doSomething();
}
}).start();
}
}
}
打印结果:
i的值为4
i的值为4
i的值为4
i的值为4
3.采用锁降级的方式处理,在写锁释放之前就加上读锁
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class TestLockDegrade {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReadLock readLock = lock.readLock();
WriteLock writeLock = lock.writeLock();
private int i;
public void doSomething() {
writeLock.lock();
//写值
i++;
readLock.lock();
writeLock.unlock();
//模拟其他耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//读值
if(i == 1) {
System.out.println("i的值为1");
}else {
System.out.println("i的值为"+i);
}
readLock.unlock();
}
public static void main(String[] args) {
TestLockDegrade lock = new TestLockDegrade();
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
lock.doSomething();
}
}).start();
}
}
}
打印结果:
i的值为1
i的值为2
i的值为3
i的值为4
手写重入锁
class MyReenterLock {
//当前对象锁是否被持有
public boolean isHoldLock = false;
//当前持有锁的线程
private Thread holdLockThread = null;
//当前线程重入次数
private int reentryCount = 0;
public synchronized void lock(){
//当前线程和持有锁的线程不是同一个线程的时候进行等待,是同一个线程则可重入
if(isHoldLock && Thread.currentThread() != holdLockThread){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
holdLockThread = Thread.currentThread();
isHoldLock = true;
reentryCount++;
}
public synchronized void unlock(){
if(Thread.currentThread() == holdLockThread){
reentryCount--;
//重入次数为0了才表示真正的释放了锁
if(reentryCount == 0){
notify();
isHoldLock = false;
}
}
}
}
public class ReentryDemo {
public MyReenterLock lock = new MyReenterLock();
public void methodA() {
lock.lock();
System.out.println("进入方法A");
methodB();
lock.unlock();
}
public void methodB() {
lock.lock();
System.out.println("进入方法B");
lock.unlock();
}
public static void main(String[] args) {
ReentryDemo reentryDemo = new ReentryDemo();
reentryDemo.methodA();
}
}
wait notify notifyAll 经典场景之生产者消费者模式
public class TestConsumerProducer {
public static void main(String[] args) {
Medium m = new Medium();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
}
public static class Consumer implements Runnable{
private Medium m;
public Consumer(Medium m) {
this.m = m;
}
@Override
public void run() {
while(true) {
m.take();
}
}
}
public static class Producer implements Runnable{
private Medium m;
public Producer(Medium m) {
this.m = m;
}
@Override
public void run() {
while(true) {
m.put();
}
}
}
/**
* 中间商
* @author renzm
*
*/
public static class Medium{
private int num = 0;
private int total = 10000;
/**
* 生产者存数据
*/
public synchronized void put() {
//判断容量是否达到最大值,达到则通知生产者停止生产
//没有达到,生产一次后就通知消费者消费
if(num < total) {
System.out.println("生产者 添加一条,当前容量="+ ++num);
}else {
//已满,当前线程不再生产
System.out.println("生产者 库存已满,当前容量="+ num);
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
notifyAll();
}
/**
* 消费者取数据
*/
public synchronized void take() {
//判断容量是否为0,是则通知消费者停止消费
//如果不为0则消费一次之后就通知生产者生产
if(num == 0) {
System.out.println("消费者 库存为0,当前容量="+ num);
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
System.out.println("消费者 取出一条,当前容量="+ --num);
}
notifyAll();
}
}
}
管道流-线程间通信的一种方式-生产者消费者模式的一种实现
管道流的主要作用就是可以进行两个线程间的通信。一个线程作为管道输出流,另一个线程作为管道输入流,在启动线程前,只需要将这两个线程的管道流连接到一起就可以。这就很方便的实现了两个线程间的通信。
package pipe;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class TestPipeStream {
public static void main(String[] args) throws IOException, InterruptedException {
Sender send = new Sender();
Receiver receive = new Receiver();
send.out.connect(receive.in);
Thread s = new Thread(send);
s.setName("发送线程");
s.start();
Thread r = new Thread(receive);
r.setName("接收线程");
r.start();
Thread.sleep(2000);
s.interrupt();
r.interrupt();
}
public static class Sender implements Runnable{
public PipedOutputStream out;
public String str = "renzhenming";
public Sender() {
out = new PipedOutputStream();
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
//调用interrupt将标志位设置位true,在Waiting和Timed Waiting状态下会发生InterruptedException异常
System.out.println(Thread.currentThread().getName()+" 发生-----------InterruptedException");
//发生中断异常时,中断标志位会被复位,我们需要重新将中断标志位设置为true,
//这样外界可以通过这个状态判断是否需要中断线程
Thread.currentThread().interrupt();
}
try {
out.write(str.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 发送-"+new String(str.getBytes()));
}
System.out.println(Thread.currentThread().getName()+" interrupt");
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static class Receiver implements Runnable{
public PipedInputStream in;
public Receiver() {
in = new PipedInputStream();
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
//调用interrupt将标志位设置位true,在Waiting和Timed Waiting状态下会发生InterruptedException异常
System.out.println(Thread.currentThread().getName()+" 发生-----------InterruptedException");
//发生中断异常时,中断标志位会被复位,我们需要重新将中断标志位设置为true,
//这样外界可以通过这个状态判断是否需要中断线程
Thread.currentThread().interrupt();
}
byte[] buff = new byte[1024];
try {
in.read(buff , 0, buff.length);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 接收到-"+new String(buff));
}
System.out.println(Thread.currentThread().getName()+" interrupt");
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
volatile关键字及其使用场景
1.能且仅能修饰变量
2.保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性,A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说,是可见
3.有序性,禁止指令重排序
使用场景
1:作为线程开关
2:单例,修饰对象实例,禁止指令重排序
volatile在jvm层的原理
volatile的底层(汇编代码)是通过lock前缀指令、内存屏障来实现的。volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。通过观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。不过,volatile变量在字节码级别没有任何区别,在汇编级别使用了lock指令前缀。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。
如何避免线程安全性问题
线程安全性问题成因
1:多线程环境
2:多个线程操作同一共享资源
3:对该共享资源进行了非原子性操作
如何避免
打破成因中三点任意一点
1:多线程环境--将多线程改单线程(必要的代码,加锁访问)
2:多个线程操作同一共享资源--不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
3:对该共享资源进行了非原子性操作-- 将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)
java多线程的happen-before原则
happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before原则中推导出来,那么他们就不能保证有序性,可以随意进行重排序。其定义如下:
1、同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
2、监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
3、对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
4、线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
5、线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
6、如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。
Java重排序的前提
在不影响 单线程运行结果的前提下进行重排序。也就是在单线程环境运行,重排序后的结果和重排序之前按代码顺序运行的结果相同。
现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
可以用 Thread 类的join方法实现这一效果。join应用场景就是一个线程需要等另一个线程执行完成后再执行
package pipe;
public class ThreadJoinDemo {
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束运行");
}
},"线程T1");
//在线程2中启动线程1,期待看到线程1结束运行后线程2再继续执行,使用Thread.join方法
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"开始运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束运行");
}
},"线程T2");
new Thread(new Runnable() {
@Override
public void run() {
thread2.start();
try {
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"开始运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束运行");
}
},"线程T3").start();;
}
}
打印结果:
线程T1开始运行
线程T1结束运行
线程T2开始运行
线程T2结束运行
线程T3开始运行
线程T3结束运行
join()源码实现
join核心其实也是通过wait方法实现的,详细见注释
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//空参join方法调用的millis都是0,所以一定会走到这里来
if (millis == 0) {
//判断当前线程是不是仍然存活,只要一直存活就一直在这里循环
//这里有一个比较容易疑惑的地方,一个就是isAlive(),我们调用的thread1.join();
//所以isAlive()判断的是thread1的存活状态,但是wait方法的作用方就比
//较容易疑惑了,从调用方来看,是thread1调用的,很容易误让人以为是要
//thread1进行等待,其实是错误的,wait的作用对象是当前调用wait的线程,所以
//虽然是thread1.join(),但是thread1.join()是在Thread2线程中调用的,所以wait作
//用的对象就是Thread2,也就是Thread2在等待
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?
多线程和并发编程中使用 lock 接口的最大优势是它为读和写提供两个单独的锁,可以让你构建高性能数据结构,比如ConcurrentHashMap和条件阻塞。
CAS相关
CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
看看AtomicInteger如何实现并发下的累加操作:
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
假设线程A和线程B同时执行getAndAdd操作(分别跑在不同CPU上):
a. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
b. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
c. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为2。
d. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。
e. 重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
CAS缺点
CAS存在一个很明显的问题,即ABA问题。
问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。
介绍一下Atomic 原子类
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示。
JUC 包中的原子类是哪4类?
基本类型
使用原子的方式更新基本类型
AtomicInteger:整形原子类
AtomicLong:长整型原子类
AtomicBoolean :布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray :引用类型数组原子类
引用类型
AtomicReference:引用类型原子类
AtomicStampedRerence:原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
简单介绍一下 AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值
AQS (AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。)
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AbstractQueuedSynchronizer -- 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。
此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。
子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。
假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。
应该将子类定义为非公共内部帮助器类,可用它们来实现其封闭类的同步属性。类 AbstractQueuedSynchronizer 没有实现任何同步接口。而是定义了诸如 acquireInterruptibly(int) 之类的一些方法,在适当的时候可以通过具体的锁和相关同步器来调用它们,以实现其公共方法。
此类支持默认的独占 模式和共享 模式之一,或者二者都支持。处于独占模式下时,其他线程试图获取该锁将无法取得成功。在共享模式下,多个线程获取某个锁可能(但不是一定)会获得成功。此类并不“了解”这些不同,除了机械地意识到当在共享模式下成功获取某一锁时,下一个等待线程(如果存在)也必须确定自己是否可以成功获取该锁。处于不同模式下的等待线程可以共享相同的 FIFO 队列。通常,实现子类只支持其中一种模式,但两种模式都可以在(例如)ReadWriteLock 中发挥作用。只支持独占模式或者只支持共享模式的子类不必定义支持未使用模式的方法。
此类通过支持独占模式的子类定义了一个嵌套的 AbstractQueuedSynchronizer.ConditionObject 类,可以将这个类用作 Condition 实现。isHeldExclusively() 方法将报告同步对于当前线程是否是独占的;使用当前 getState() 值调用 release(int) 方法则可以完全释放此对象;如果给定保存的状态值,那么 acquire(int) 方法可以将此对象最终恢复为它以前获取的状态。没有别的 AbstractQueuedSynchronizer 方法创建这样的条件,因此,如果无法满足此约束,则不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行为当然取决于其同步器实现的语义。
此类为内部队列提供了检查、检测和监视方法,还为 condition 对象提供了类似方法。可以根据需要使用用于其同步机制的 AbstractQueuedSynchronizer 将这些方法导出到类中。
此类的序列化只存储维护状态的基础原子整数,因此已序列化的对象拥有空的线程队列。需要可序列化的典型子类将定义一个 readObject 方法,该方法在反序列化时将此对象恢复到某个已知初始状态。
AQS 原理概览
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
ThreadLocal
ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问。
当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
package pipe;
import java.util.function.Supplier;
public class ThreadLocalDemo {
private ThreadLocal num2 = new ThreadLocal();
private ThreadLocal num = ThreadLocal.withInitial(new Supplier() {
@Override
public Integer get() {
return 0;
}
});
public void inscrease() {
Integer i = num.get();
i++;
System.out.println(Thread.currentThread().getName()+" i="+i);
num.set(i);
}
public static void main(String[] args) {
ThreadLocalDemo local = new ThreadLocalDemo();
for(int i = 0;i<3;i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
local.inscrease();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}
打印结果:
Thread-1 i=1
Thread-0 i=1
Thread-2 i=1
Thread-0 i=2
Thread-2 i=2
Thread-1 i=2
Thread-0 i=3
Thread-1 i=3
Thread-2 i=3
Thread-2 i=4
Thread-0 i=4
Thread-1 i=4
Thread-0 i=5
Thread-1 i=5
Thread-2 i=5
Thread-1 i=6
Thread-2 i=6
Thread-0 i=6
Thread-1 i=7
Thread-0 i=7
Thread-2 i=7
Thread-0 i=8
Thread-1 i=8
Thread-2 i=8
Thread-0 i=9
Thread-2 i=9
Thread-1 i=9
源码简单分析
主要是两个方法set和get,可以看到,在set值的时候,会获取到当前线程,然后将值保存在这个线程的ThreadLocalMap中,get值的时候也是先获取到当前线程,然后拿到这个线程中的ThreadLocalMap对象,再从ThreadLocalMap中取出这个值,值是和当前线程绑定的,那么每个线程中的值都是无关的了。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
简单应用场景
复杂逻辑下对象传递,即解决共享参数
当函数调用栈比较深以或代码入口的多样性,又需要监听器能够贯穿整个线程的执行过程,这样就会将业务变的臃肿复杂化。常规操作就是将监听器通过参数的形式在函数调用栈中进行传递或将监听器作为静态变量供线程访问,这两种方式的结果就是:第一种将造成函数调用栈很深及;第二章将造成不具有可扩充性。而采用TheadLocal就解决这两种方式带来的问题。一下将例举常规方式和使用TheadLocal来解决这个问题。
比如,下边这个场景,在请求接口的时候,有很多方法都需要传递监听器进去从而在有结果的时候回调出来,常规的做法就是在每个方法的形参上传入这个监听器,但是这样显然不够优雅,我们完全可以通过ThreadLocal来保存监听器对象,然后在每个方法中取出对象进行回调,当然用HashMap ArraryList都可以实现,但是用ThreadLocal能保证不同线程之间互不干扰
package pipe;
import java.util.function.Supplier;
public class ThreadLocalDemo {
private static OnCallListener l = new OnCallListener() {
@Override
public void onSuccess(String s) {
System.out.println("success "+s);
}
@Override
public void onFailed(String f) {
System.out.println("failed+ "+f);
}
};
private static ThreadLocal local = ThreadLocal.withInitial(new Supplier() {
@Override
public OnCallListener get() {
return l;
}
});
private interface OnCallListener{
void onSuccess(String f);
void onFailed(String f);
}
private OnCallListener listener;
public void setOnCallListener(OnCallListener l) {
this.listener = l;
}
public static void main(String[] args) {
//1.请求网络接口返回成功
int i = 2;
if(i == 0) {
method1();
}else if( i == 2) {
method2();
}else if( i == 3) {
method3();
}else {
method4();
}
}
public static void method1() {
OnCallListener l = local.get();
l.onSuccess("method1");
}
public static void method2() {
OnCallListener l = local.get();
l.onFailed("method2");
}
public static void method3() {
OnCallListener l = local.get();
l.onSuccess("method3");
}
public static void method4() {
OnCallListener l = local.get();
l.onFailed("method4");
}
}
ThreadLocal中为什么不用HashMap代替ThreadLocalMap
HashMap如果发生hash冲突,通过拉链法形成一个单链表,而ThreadLocalMap不是这样,ThreadLocalMap底层也是数组,但是发生hash冲突的时候,他会通过线性寻址的方法找到下一个不冲突的位置,不会形成链,也不能形成链
Condition
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TestConsumerProducer {
public static void main(String[] args) {
Medium m = new Medium();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Consumer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
new Thread(new Producer(m)).start();
}
public static class Consumer implements Runnable{
private Medium m;
public Consumer(Medium m) {
this.m = m;
}
@Override
public void run() {
while(true) {
m.take();
}
}
}
public static class Producer implements Runnable{
private Medium m;
public Producer(Medium m) {
this.m = m;
}
@Override
public void run() {
while(true) {
m.put();
}
}
}
/**
* 中间商
* @author renzm
*
*/
public static class Medium{
private int num = 0;
private int total = 100;
private ReentrantLock lock = new ReentrantLock();
Condition producerCon = lock.newCondition();
Condition consumerCon = lock.newCondition();
/**
* 生产者存数据
*/
public void put() {
lock.lock();
try {
//判断容量是否达到最大值,达到则通知生产者停止生产
//没有达到,生产一次后就通知消费者消费
if(num < total) {
System.out.println("生产者 添加一条,当前容量="+ ++num);
}else {
//已满,当前线程不再生产
System.out.println("生产者 库存已满,当前容量="+ num);
try {
producerCon.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
consumerCon.signalAll();
}finally {
lock.unlock();
}
}
/**
* 消费者取数据
*/
public void take() {
lock.lock();
try {
//判断容量是否为0,是则通知消费者停止消费
//如果不为0则消费一次之后就通知生产者生产
if(num == 0) {
System.out.println("消费者 库存为0,当前容量="+ num);
try {
consumerCon.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
System.out.println("消费者 取出一条,当前容量="+ --num);
}
producerCon.signalAll();
}finally {
lock.unlock();
}
}
}
}