java-concurrent
- java-concurrent
- thread
- thread-safe
- juc
- interview
- synchronied代码范式
- 如何停止一个正在运行的线程
- java如何实现多线程之间的通讯和协作
- wait,sleep,join的区别
- 自己实现线程池、连接池
- 什么是可重入锁(ReentrantLock,写一个方法实现测试synchronized的可重入
- 当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
- synchronized,wait,notify的关系? 代码实现2个线程交替打印数据
- 两个线程交替打印,依次输出0-100? 换成n个线程呢?
- 乐观锁/悲观锁; 排他锁/共享锁; 公平锁&非公平锁
- CopyOnWriteArrayList应用场景
- servlet线程安全
- ThreadLocal
- next
thread
线程的状态
// Thread.State
/**
// 竞争monitor
BLOCKED,
/**
* wait线程被notify,进入竞争队列,处于BLOCKED状态
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
* 1. 等待某个线程通知
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object.
* 2. 等待调用线程终止
* A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
***** 调用wait()方法的线程 ****
* sleep || 有明确的等待时间
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
- blocked: 非公平竞争monitor
- waiting(当前线程放弃monitor,等待通知)
- Object.wait with no timeout
- Thread.join with no timeout
- LockSupport.park
- time_waiting(有等待时间,不放弃monitor)
- Thread.sleep
- Object.wait with timeout
- Thread.join with timeout
- LockSupport.parkNanos
- LockSupport.parkUntil
waiting/time_wait -> blocked -> running
线程同步
- AbstractQueuedSynchronizer(AQS): 抽象队列同步器
- Wait,NotifyAll,synchronized
- volatile和消息(管道:Pipe),锁
- 工具类
- CountDownLatch
- CylicBarrier
- Semaphore
- BlockingQueue(5)
- ArrayBlockingQueue: 数组,有界
- LinkedBlockingQueue:链表,有界
- PriorityBlockingQueue:优先级,无界
- DelayQueue:有序,无界
- SynchronousQueue:不存储元素
Thread代码分析
//进程的堆空间是多线程共享的,多线程修改进程堆空间的数据就会出现线程安全问题。
Class A{
// 线程不安全,static不具备线程安全属性
private static Integer i1 = 1;
//一旦初始化,不可被修改,但需要注意引用/值
private final Integer FLAG = 1
// 堆空间,线程不安全
private Integer i1 = 2;
// function stack
public String print(String x){
String y = x;// java栈空间,线程安全
System.out.pritntln(y);
}
}
- sleep(): 类的
native方法,使当前线程进入TIMED_WAITING状态,被中断时抛出InterruptException,当前线程放弃monitor - join(): 底层调用wait,阻塞main线程waniting状态
- yield(): 当前线程让出CPU, yield的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行
- interrupt():这是中断位
- runnable状态:只修改中断位,isInterrupted()判断终端位
- Time-waiting状态:抛出InterruptEXception,并break
- stop()
- 抛出JVM的异常,已经丢弃(解锁失败、执行中任务失败)
线程结束运行的方式?
- 设置共享标记位volatile
- 通过设置/判断中断位:注意判断线程的状态
- stop()异常;不推荐
thread-safe
lock
Java中锁的实现原理可以分为三个级别:
- JVM级别: synchronized(映射系统mutex)
- 汇编级别: volatile
- 代码级别: Lock
final
- static: 全局只有一份
- final: 指定的
值(或引用)不可变; 内存指令禁止重排规则(JMM内存模型中的storestore,loadload) - 所有的final修饰的字段都是编译期常量? 不是
- final重排序的规则
// 1. 所有的final修饰的字段都是编译期常量吗
// 非编译期常量
Random r = new Random();
final int k = r.nextInt(); // 引用的值可以变
// 2. final修饰的不会发生类型cast
final byte b1=1;
final byte b2=3;
byte b3=b1+b2;//不会出错,相信你看了上面的解释就知道原因了。
// 3. 最简单的指令重排规则
class B{
private int a;
private final int b;
B(){
a = 1; // 被重拍排到构造函数之外
b = 2; // 不会被重拍
}
}
b = new B; // 执行过程
- 分配空间B
- 初始化 final b
- 赋值b
- 初始化 a,被重拍排到构造函数之外
volatile
- 只保证
可见性和有序性, 不保证原子性- 内存屏障 => 可见性
- happpen-before => 有序性
- 指令重排可能发生的地方
- 编译器(class),jvm字节码重新排序优化执行
- cpu(
lock cmpxchg)
- 缓存一致性协议(MESI) => 内存屏障指令; (总线锁开销太大-总线风暴,已经废弃)
- volatile的指令重排规则 =>
- 是一种轻量级同步机制,不会像schronized一样,=>
不存在线程的上下文切换 - 原子操作时需要使用Atomic
可见性
JMM => 通过内存屏障指令实现(MESI)
// volatile自增加场景v++,下面是c的汇编指令
mov 0xc(%r10),%r8d ; Load, get v
inc %r8d ; Increment, v+1
mov %r8d,0xc(%r10) ; Store, set v
lock cmpxchg $0x0,(%rsp) ; StoreLoad Barrier
// 只在此行处内存可见, 但是这4条指令整体不具备原子性
// 在原来的汇编治理基础上追加了lock指令,设置barrier; 没有线程切换,但是整体不具备原子性
- 多核场景下,将当前cpu-cache的数据写回主内存
- 其他核缓存的对应值置为无效
有序性-禁止重排
//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1 线程A修改共享变量
flag = true; // 2 线程A写volatile变量
}
public void reader() {
if (flag) { // 3 线程B读同一个volatile变量
int i = a; // 4 线程B读共享变量
}
}
}
- 根据程序次序规则:1 happens-before 2, 3 happens-before 4。
- 根据 volatile 规则:2 happens-before 3; => volatile写 先于 volatile读
- 根据 happens-before 的传递性规则:1 happens-before 4
指令指令重排规则,如何保证有序性
单例解析
// 双重校验锁实现对象单例
public class Singleton {
// 私有,单例,禁止指令重排
private static volatile Singleton uniqueInstance;
// 不能构造
private Singleton() {}
// 双重校验锁
public static Singleton getUniqueInstance() {
// 先判断对象是否已经实例过,没有实例化过才进入加锁代码=>避免锁竞争
// 减少没有必要的竞争
if (uniqueInstance == null) {
// 类对象加锁=>原子性
synchronized (Singleton.class) {
if (uniqueInstance == null) {
// 初始化为什么加锁
// 使用volatile禁止指令重排
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
初始化为什么加锁(构造函数执行的过程,3个指令)
uniqueInstance = new Singleton();这段代码其实是分为三步执行
- 为Singleton分配内存空间(heap)
- 初始化Singleton(设置属性值等)
- 将uniqueInstance指向分配的内存地址
加锁保证这三步的原子性。
uniqueInstance采用volatile关键字修饰
但是由于JVM具有指令重排的特性,执行顺序有可能变成1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。(已经加锁了,没必要volatile)
使用volatile 可以禁止JVM的指令重排(构造函数有三个指令),保证在多线程环境下也能正常运行;
应用
/**
从dubbo学习到的valatile容器dubbo里面的奇妙的应用
思考VolatileHolder<Map<String,Object>+synchronized与ConcurrentMap<String,Object>的区别
*/
public class VolatileHolder<T> {
private volatile T value; // 内存可见
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
VolatileHolder h = new VolatileHolder<Map<String,Object>();
synchronized(h){ // 不怕初始值=null}
synchronized
- object(唯一) -> monitor -> synchronized; 一把锁(object.monitor)只能同时被一个线程获取
- monitor应该是只class Header里面的
mark-word, 协同栈祯里面的lock-record共同实现锁????? javap -verbose SynchronizedDemo.class查看classmonitorenter和monitorexitjava字节码指令 => 包装synchronized代码块 => 调用OS底层mutexACC_SYNCHRONIZED=> 标识synchronized方法=> 调用OS底层mutex- 获取锁失败,线程进入
BLOCK状态,挂起线程和恢复线程的操作都需要转入内核态中完成(用户=>内核=>用户) - Synchronized本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理
- 多个线程等待同一个snchronized锁的时候,JVM如何选择下一个获取锁的线程
- 非公平锁,可重入
- jvm.thread(tid) => native thread(nid); synchronized => mutex
加锁
- 普通方法: final
- static方法:给当前类加锁,同时会作用于类的所有对象实例; class在PG,相当于全局锁
- 代码块:括号里面的对象/类加锁(不能为null),一般会选择公有的类,结合wait/notify实现同步
尽量不要使用
synchronized(String a)因为JVM中,字符串常量池具有缓存功能, 使用final Object mutex = new Object;String适合map,不适合synchronized
底层
- 无锁->偏向锁->轻量级锁(cas)->重量级锁(mutex); 为了提供获取锁和释放锁的效率
- mutex设计用户态/内核态的交互
JVM优化
synchronized的底层是通过操作系统的mutex锁实现。 mutex的使用涉及线程context切换(用户态<>内核态)。开销很大;
JVM为此做的优化:锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning).
加锁过程:无锁->偏向锁->轻量级锁(cas)->重量级锁(mutex)。
-
锁粗化:减少不必要的紧连在一起的unlock/lock操作,将多个连续的锁扩展成一个范围更大的锁
-
锁消除:
运行时JIT编译器的逃逸分析来消除=>虽然加了synchronized,但是不会发生线程同步 -
轻量级锁(cas):monitorenter和monitorexit
通过CAS原子指令实现锁的获取/释放,不用切换context,synchronized代码在单线程环境 -
偏向锁(Biased Locking):锁获取过程中...
-
适应性自旋(Adaptive Spinning):轻量级锁(cas)->重量级锁(mutex)切换
时,尝试一定的次数(适应性)的自旋(spinning) -
自旋锁:cas
-
自适应自旋锁: cas +
自旋时间(次数)适应 -
锁消除:java-api代码里面有sync,但是(JIT逃逸分析发现)自己的代码不会同步,运行时去掉了sync(
javap查看class) -
锁粗化: 大sync代替小sync
// javap 查看class
public static String test04(String s1, String s2, String s3) {
// 锁粗化:的优化场景
StringBuilder sb = new StringBuilder();
// 编译后: 用一个synchronized代替3个小的
sb.append(s1); // append方法内部有synchronized
sb.append(s2);
sb.append(s3);
return sb.toString();
}
- 轻量级锁:
object header两部分组成
mark-word: HashCode、GC Age、锁标记位、是否为偏向锁kclass-point: 指向方法区
栈帧 => lock-record
cas
- 偏向锁:
应用
对比lock
- synchronized只有代码执行完毕或者异常结束才会释放锁
Lock可以中断和设置超时- synchronized每个锁仅有一个单一的条件,
Lock能与Condition结合- synchronized无法知道是否成功获得锁,锁的信息都保存在对象头里
- 能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类
- synchronized实际上是非公平的.可以预防饥饿
在使用notify()/notifyAll()方法进行通知时,被通知的线程是由JVM选择的;用ReentrantLock类结合Condition实例可以实现选择性通知,这个功能非常重要,而且是Condition接口默认提供的
中断
一个线程等待时间过长,它可以中断自己(Lock),然后ReentrantLock响应这个中断,不再让这个线程继续等待.
wait-notify
- wait,notify,notifyAll(不是锁,用来处理线程同步)
- wait(),notify()和notifyAll()都是java.lang.Object的native方法
- 每个java的Object都是锁,编译的时候映射了一个Monitor对象,任何一个时刻,对象的控制权(Monitor)只能被一个线程拥有(owner)
- 调用这些方法时必须获取对象的monitor,代码表现是 => 使用synchronized保护这个对象,synchronized就是竞争获取该对象的Minotor;
wait,notify是对象方法,不是类方法,synchronzied需要保护当前对象=>synchronized最好使用对象- wait()会使当前线程放弃该对象的锁(Monitor),处于WAITING状态并且不能继续参与竞争,除非被notify()或超时,线程变成Blocked状态,才能继续参与竞争monitor.
- notify(),notifyAll(),不会立刻释放锁,只是
通知某个或者所有wait该对象monitor的线程(变成BLOCKED)可以重新参与竞争该对象的锁(Monitor); 如果获得成功则从wait()方法处开始继续运行。 - The current thread must own this object's monitor.所以,永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成IllegalMonitorStateException(当前线程没有拿到该对象的Monitor)
public static void testWaitNotify() {
Thread wait = new Thread(new WaitThread(), "WaitThread");
Thread notify = new Thread(new NotifyThread(), "NotifyThread");
try {
wait.start(); // wait线运行
TimeUnit.SECONDS.sleep(1);
notify.start(); // notify后运行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 测试wait
static class WaitThread implements Runnable {
@Override
public void run() {
// wait线程,获取flag对象的monitor
synchronized (flag) {
while (flag) {
try {
System.out.println(Thread.currentThread() + " ,flag = true @" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
// 此处调用flag对象的wait(),已经使用synchronzed保护该对象
// flag.wait()使当前wait线程放弃flag的monitor对象,等待接收到其他线程的notifyAll时,重新获取对象flag的monitor,继续从此处运行(也可以超时自己返回,建议在while循环内判断)
// 线程WaitThread处于WAITING状态,不能继续竞争flag的Monitor,等被notify时,变成Blocked状态,才能继续竞争
flag.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread() + " ,flag = false @" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
// 测试notify
static class NotifyThread implements Runnable {
@Override
public void run() {
// 先获取flag对象的锁,因为要调用flag.notify()方法
synchronized (flag) {
try {
System.out.println(Thread.currentThread() + " ,hold flag monitor first @" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
// 发出通知,但是不会立刻释放flag的monitor,
// 只有当前线程释放了flag的monitor,wait的线程才会从wait方法被调用的地方,重新竞争flag的monitor继续执行
flag.notifyAll();
flag = false;
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 结束时第一次释放flag的monitor
/*
// 这段代码能让被wait的waitThread先获取flag的锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
// 第二次竞争获取flag的monitor(与waitThread线程的flag.wait()方法处竞争)
synchronized (flag) {
try {
System.out.println(Thread.currentThread() + " ,hold flag monitor second @" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}// 结束时第二次释放flag的monitor
}
}
总结话术以上代码
- 竞争获取对象Monitor的地方2个, synchronzed的入口处(monitorenter), 和wait()调用处(只有被notify之后,WAITING变为BLOCKED,才有权参与竞争),2处的竞争关系平等.
- 释放对象Monitor的地方1个: synchronzed的出口处(monitorexit),notify只通知状态变化并不释放Monitor。
- notify只负责通知,不释放对象Monitor(等到执行结束),通知wait()调用的地方可以参与竞争该对象的Mointor了
- 对象的Monitor只能被一个线程持有
- wait,notify被调用时需要先对该对象加锁,线程A调用x.wait()时,线程A会放弃x的Monitor,同时线程A进入WAITING状态,不参与竞争Monitor;当线程B获得x的Monitor时调用x.notifyAll(),
但是不会立刻释放x的Monitor;A接收notify后变成Blocked状态,参与竞争对象x的Monitor,竞争成功后从wait处继续执行,
juc
java.util.concurrent
atomic
并发包java.util.concurrent的原子类都存放在java.util.concurrent.atomic
-
实现
- 基于循环CAS(CompareAndSwap)实现(has-a Unsafe类,CPU指令)
- 底层调用原生的cas接口,是
基于CPU的CMPXCHG(指令本身支持CAS)指令实现的,也是CPU级别的实现 - 具有原子性=>比较volatile(volative只保证可见性,不保证原子性)
-
循环CAS存在的问题
- 循环等待时间长(本身固有的问题)
- ABA问题,Atomic
StampedReference可以解决
// 基于CAS指令
public class AtomicInteger extends Number implements java.io.Serializable {
// CAS的实现:has-a
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 值和内粗偏移
private volatile int value;
private static final long valueOffset;
static {
try {
// 获取内存偏移
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
}
// ABA问题存在是因为CompareAndSwap的值(值可以复现A->B->A),解决的办法是CompareAndASwap值的版本/时间戳(只能增加)。
// 通过 AtomicStampedReference 解决ABA问题
private static class Pair<T> {
final T reference;
// 封装了版本号,同时cas了stamp属性,stamp只能增加,不能减少
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
aqs
- AQS解决等待排队问题(AQS是blocked竞争队列,Condition是waiting队列)
- unsafe实现CAS,解决同步; Atomic对象只需要同步
支持自己编程实现的自己的锁,继承接口AQS(AbstractQueuedSynchronizer);这个类在java.util.concurrent.locks包下面。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// CLH虚拟的双向队列
static final class Node{};
private transient volatile Node head;
private transient volatile Node tail;
// 锁状态
private volatile int state;
// cas实现
private static final Unsafe unsafe = Unsafe.getUnsafe();
}
AQS实现的核型思想
AQS实现的核型思想是state+CLH队列
- state控制共享资源的状态(空闲/占有),通过CAS(unsafe)实现
- CLH,是一个虚拟的双向队列,AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现
线程阻塞,等待,排队。
AQS定义两种资源共享方式
-
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
-
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock。
CLH同步机制
- SMP,NUMA
- CLH,MCS
CLH和MCS锁的实现
CLH有点像ConcurrentHashMap(jdk-1.8)的分段锁: 使用双联表记录等待队列,使用锁对连表的第一个节点cas.
自定义AQS
同步器的设计是基于模板方法模式的;
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
需要重写的方法
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
AQS组件
- Semaphore
- CountDownLatch
- CyclicBarrier
- ReentrantLock,SynchronousQueue,SynchronousQueue
- 自己构造自己的同步器
public class ReentrantLock implements Lock, java.io.Serializable {
// 依赖:has-a
private final Sync sync;
// 继承AQS
abstract static class Sync extends AbstractQueuedSynchronizer {}
// 非公平锁
static final class NonfairSync extends Sync {}
// 公平锁
static final class FairSync extends Sync {}
}
interview
synchronied代码范式
/**
* 1. Lock编程范式
*/
Lock lock = new ReentrantLock();
lock.lock()
try{}finally{lock.unlock()};//lock.lock()在try外,获取锁也有可能异常
// 使用单线程实现的死锁的代码
Lock lock = new Lock(); // 锁内竞争锁
lock.lock();
try{
//此处重新竞争锁lock,会出现死锁; 这样能实现的条件是lock是排他的且不支持重入的,WriteLock,synchronized都是是支持重入的
lock.lock();
}finally{lock.unlock();}
/**
* 2.synchronized编程范式
*/
// 同步使用上Lock+Condition等价于sychronized+wait/notify
// wait-范式
volatile 条件c=false;
synchronized(对象a){
while(条件不满足:!c)){
对象a.wait(); // 等待A
}
}
// notify-范式
synchronized(对象a){
c = true;
对象a.notifyAll();
}
// 超时等待-范式(多个条件的方式)
const future = System.currentTimes() + mills
volatile remain = mills
sycchronzed(a){
// 此处可以增加其他的条件
while(!c && reamin >0){
a.wait(remain)
remain = future - system.currentTimes();
}
}
如何停止一个正在运行的线程
- 共享标记位
- 中断(推荐)
- stop/resume/suspend等已经作废的方法
- 正常停止;
//stop()丢弃的原因:Stop会抛出JVM的异常,并释放所有的锁,导致锁不可控
//resume/suspend:native方法,不能释放锁资源,可能导致dead lock
@Deprecated
public final void stop() {
// 检查停止当前线程的权限
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
if (threadStatus != 0) {
resume(); // Wake up thread if it was suspended; no-op otherwise
}
// 锁没法把控,当前thread的任务不好把控
// 停止线程的方式,通过抛出ThreadDeath异常,属于系统级异常,只能由JVM处理
// 导致当前线程释放所有的锁,不可控
// 中断抛出的异常时 InterruptiException可以捕获处理
stop0(new ThreadDeath());
}
// Error异常由JVM捕获
public class ThreadDeath extends Error {
private static final long serialVersionUID = -4417128565033088268L;
}
java如何实现多线程之间的通讯和协作
wait,sleep,join的区别
- wait(),对象方法,调用wait()的线程必须首先获取对象的monitor,调用时当前线程会放弃对象的monitor,线程进入WAINTING状态
- sleep()是static,native类方法,调用的时候,
当前线程不会丢弃任何monitor和wait的底层实现不一样(JDK源码native),线程进入TIMED_WAITNG状态 =>sleep等待时间结束必须持有monito继续执行; - t.jion(),底层调用了wait; main线程调用了thread对象t的wait(),使main线程处于Waiting,等待t
WAITING,TIME_WAITING状态
- WAITING:wait() -> 当前线程放弃monitor
- TIME_WAITING:sleep() -> 当前线程不放弃monitor,等待时间结束就继续执行
自己实现线程池、连接池
注意点:ThreadExecutorPool的设计思路(就是execute的设计思路:3步,有哪些优点)
本质:保证线程安全的情况下对线程池和任务队列的访问
什么是可重入锁(ReentrantLock),写一个方法实现测试synchronized的可重入
(synchronzed方法的递归调用) 可重入也叫递归锁,
当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
能进入其他的普通方法,这些方法不需要获取对象的Monitor
synchronized,wait,notify的关系? 代码实现2个线程交替打印数据
线程调用wait,notify需要获取对象的Monitor,需要synchronized竞争; wait调用时使线程处于WAITING状态,并释放Monitor;notify通知waiting的线程进入Bolcked状态并可以重新竞争锁
// 永久等待的编程范式
synchronized (obj) {
// 因为要调用obj.wait的方法,此处只能是当前对象obj,不能是class
while (condition not ok)
obj.wait(timeout) // 是对象的方法,不是类方法
}
// wait结束的方法,1:其他线程notify,2:中断,3:时间到了; 1&3是会重新竞争锁,2直接抛出中断异常
两个线程交替打印,依次输出0-100? 换成n个线程呢?
tid-A:0
tid-B:1
tid-A:2
//wait条件范式;tid是线程编号,num = 0(可以是volatile,Atomic);
while (num < 100) {
synchronized (obj) {
while (num % count != nid) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getId() + "," + nid + ":=>" + ++num);
obj.notifyAll();
}
}
// 实现顺序
乐观锁/悲观锁; 排他锁/共享锁; 公平锁&非公平锁
- 乐观锁:基于
数据版本做cas - 悲观锁:一种排他锁,维护绝对串行,synchronized,直到自己释放,否则其他线程只能等待
- 排他: 同一时刻只允许一个线程访问同步区,如写锁
- 共享: 同一时刻允许多个线程访问同步区,如读锁;AQS内部有2个常量标记SHARED,EXCLUSIVE
- 公平: 基于等待队列FIFO,有等待线程时不竞争锁,去排队
- 非公平锁调用时就竞争锁,不判断队列
CopyOnWriteArrayList应用场景
多线程修改list
servlet线程安全
看情况:
出现实例变量和static变量的时候,线程不安全;局部变量不会出现问题。
-
Servlet被设计为单例,但是运行在多线程环境(tomcat)中
-
当Servlet里面存在局部变量的时候,就会出现线程安全问题;在不同的线程中,局部变量的值称为不同的状态,没有这些局部变量的Servlet是无状态对象,是线程安全的
如果出现局部变量,引用变量,static变量的时解决方法
- 使用锁
- ThreadLocal