Java并发编程
Thread
Thread的几种状态
- NEW:线程刚创建还没start
- RUNNABLE: 线程正在JVM中执行
- BLOCKED: 当前线程正在等待monitor lock Synchronized代码块会让没获得锁的线程进入Blocked状态,Object.wait也会进入BLOCKED状态。
- WAITING: 等待其他线程唤醒。Locksupport.park会让线程进入waitting状态,Object.wait() Thread.join()
- TIMED_WAITING: sleep(1000)、wait(1000)指定时间的阻塞线程都是进入这个状态
- TERMINATED: 线程结束
WAITTING和BLOCKED的区别
BLOCKED是指线程正在等待获取锁;WAITING是指线程正在等待其他线程发来的通知(notify)。
个人理解不知道对不对:Blocked线程在自旋尝试获取锁,Waittint状态在等着其他地方唤醒。???
线程阻塞/中断 方法
- sleep: Thread.sleep(4000);线程进入TIMED_WAITTING状态,sleep不会释放锁
- wait: 只能在同步代码块中使用,调用obj.wait之后进入了waitting状态。wait通过notify/notify唤醒。线程没有引用object调用object.notify/wait等方法会抛出IlleagalMoniterException
- yield: 让当前线程让掉占用CPU的时间段,重新进入就绪状态和其他线程重新竞争CPU,yield可以暂停当前线程,如果当前线程已经持有锁和sleep一样不会释放锁。
- join: 可以让线程进入waitting状态
- await:Lock中Condition中使用,用于阻塞进程和signal配合使用。
线程唤醒方法
- notify/notifyAll: object中方法只能在同步代码块种使用,用于唤醒wait
- interrupt: thread.interrupt会让线程在阻塞的地方抛出interruptException变相的唤醒线程,通知线程做释放资源的工作。
- singal: Lock中Condition中使用,用户唤醒阻塞进程,和await配合使用。
怎样正确停止线程
- 线程中run方法执行完之后线程就会销毁,对于一直在循环执行的线程可以通过设置变量来停止线程。
- 调用thread.interrupt方法停止线程。调用interrupt方法后不会立刻停止线程,需要判断isInterrupt是true/false来决定下一步工作。
@Override
public void run() {
for(int i=0;i<500000;i++) {//只有循环足够多,才能看到线程停止
if(this.interrupted()) {//用Thread.interrupted有什么不同?
System.out.println("已经是停止状态了!我要退出");
//break; for循环后面如果有语句会继续执行
try {
throw new InterruptedException();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("i="+(i+1));
}
}
wait 和sleep的区别
wait是Object中的方法,sleep是Thread中的方法
wait方法会让出锁,调用notify/notifyAll方法才会重新去竞争锁,sleep则不会让出锁
wait只能在同步代码块中使用,sleep可以在任何地方使用
死锁
线程一拿到A对象的锁,线程二拿到B对象的锁,同步代码执行过程中需要依赖对象B的锁,而依赖A对象的锁,此时就产生了死锁。
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("thread1...");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("thread2...");
}
}
}
});
t1.start();
t2.start();
怎么避免死锁:尽量不要在一个锁内尝试去拿另一个对象的锁。
线程不安全 && 线程安全
线程安全:多线程访问一段代码不会产生不确定的结果,称为线程安全。线程安全都是由全局变量和静态变量引起的。
成员变量:存在于堆内存中,和对象同时创建,堆内存被所有线程共享。
类变量:被static关键字修饰的变量,只要类存在变量就可以访问到。无论类创建了多少个对象,类变量只有一个,被static修饰的变量存放在方法区中。方法区中的变量所有的线程都可以访问
局部变量:每个线程中都包含一个栈内存区域,局部变量存储在此区域,栈内存区域每个线程是私有的,其他线程无法访问到,当方法执行完成后会释放掉内存,所以局部变量是线程安全的。
怎么保证线程安全
通过atomic包提供的类
atomic包下常用的有AtomicInteger,AtomicBoolean,AtomicRefrence几种,通过调用类中提供的api来修改对象(变量)可以保证修改操作的原子性,从而保证线程安全,是实现线程安全最轻量级的方法。
atomic内部是通过CAS+validate来保证原子性。
//AtomicInteger AtomicBoolean
private volatile int value;
//AtomincRefrence
private volatile V value;
//通过调用Native方法保证修改值的原子性 获取失败时会自旋
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;
}
volatile
volatile只能保证内存可见性,但是不能保证原子性。在一定情况下使用volidate可以提高性能,因为使用volatile之后多线程访问变量不需要切换线程。
CAS
CompareAndSwap(比较并交换)通过比较某个内存地址中的值是否等于预期的值,如果相等则将新的值付给该内存,不相等则不赋值。当多个线程对同一个内存地址进行CAS操作时只有一个线程能成功,其余的会失败,失败的线程会重新尝试,通过硬件支持实现。
Synchronized
Synchronized属于独占锁,每次进入临界区时都会考虑拿不到锁的情况,拿不到锁会让线程进入阻塞状态,在多线程竞争下频繁的加锁和释放锁操作会引起性能问题。Synchronized在思想上属于悲观锁。
CAS实现属于乐观锁的思想,每次都假设没有冲突,如果有冲突了就重试直到成功为止。
Synchronized分为对象锁和类锁两种。
对象锁
每个对象都只有一把锁,当线程A执行某个对象的同步方法时,此时该对象唯一锁被线程A占用,线程B如想要进入该对象的同步方法执行则必须等线程A释放锁,但是线程B可以访问该对象的非同步方法。使用Synchronized关键字修饰加锁和解锁的过程都是由JVM控制,不需要上层关心。
//对象锁两种实现方式
public synchronized void objectLock() {
}
public void objectLock() {
synchronized (this) {
}
}
类锁
类可以创建多个实例,当类第一次被加载时会创建class对象,这个class对象时唯一的,该类的所有实例共用同一个class对象,类锁使用的是class对象的锁,所以类锁只有一把。
//类锁的两种实现方式
public static synchronized void staticLock() {
}
public void staticLock1() {
synchronized (MyClass.class) {
}
}
Synchronized底层实现和优化
Synchronized是Java的关键字,通过在编译时期在class文件中插入moniterEnter/moniterExit来控制了访问逻辑。
Synchronized属于可重入锁(同一线程多次进入同步代码块不需再次获取锁)。同时刻一个对象的监视器只能被一个线程获得,获得对象监视器的线程可以进入到同步代码块种执行。没有获取对象监视器的线程会进入Blocked状态,当对象的监视器被释放时,其他线程就有机会获取到该监视器。
jdk1.6在每个Java对象头中markword字段中记录了当前锁的状态,通过控制锁的状态来提升Synchronized性能。
锁的几种状态:无锁,偏向锁,轻量级,重量级。这几种状态会随着线程竞争的激励程度来膨胀(JVM控制),但是不能降。
-
无锁: 默认是无锁状态。
-
偏向锁:大多数情况下锁不存在竞争,而是总是有同一线程获得,为了让获得锁的代价更低引入了偏向锁。当线程A进入同步代码块时会将该线程A的ID写入改对象头部信息中。每个线程在进入代码块前都会先检测当前对象锁是否被占用,如果被占用则比较对象中存储的线程的ID,如果Id和之前存储在头部中的Id相同,则证明是同一线程进入不再进行加锁操作,直接进入代码中执行。如果不是同线程,则此时证明发生了线程间竞争,偏向锁会膨胀成为轻量级锁,让线程之间进行竞争。偏向锁加锁和解锁只有一次,第一个线程进入时加锁,但是退出同步代码块时并不会解锁,只有当竞争线程出现时才会解锁。 偏向锁的释放的开销相对来说挺大,所以偏向锁适用于大部分都是同一个线程执行的情况。
-
轻量级锁:有竞争线程出现时偏向锁膨胀为轻量级锁。此时有多个线程在竞争同一把锁,轻量级锁每次进入和退出代码块都需要加锁和解锁操作。竞争线程发现锁已经被其他线程占用后,会自旋(一直尝试获取锁)长时间的自旋会消耗CPU资源,所以经过一段时间自旋后如果还拿不到锁,会继续膨胀成重量级锁。
-
重量级锁:当自旋一定时间后,会膨胀为重量级锁。会把一直未取得锁的线程阻塞,直到线程释放锁后才会被唤醒继续执行,因为线程处于阻塞状态当其他线程释放锁时并不会立刻响应。
总结:
jdk1.6版本Synchronized关键字加入锁的状态判断后性能有很大提升。
偏向锁引入提高了于大部分只有一个线程访问临界区时的性能。1.6之前未拿到锁的线程处于Blocked状态,锁放释放后需要唤醒Bloked的线程,轻量级锁在一直自旋,锁被释放后会其他线程会立刻得到锁。重量级锁则解决了当同步代码块中执行耗时工作其他线程迟迟拿不到锁一直自旋引起CPU的消耗的问题。
ReentrantLock
https://blog.csdn.net/qq_38293564/article/details/80515718
ReentrantLock和Synchroniezed都属于可重入锁,Lock是Java层实现,相比于Synchronized更加灵活。
ReentrantLock分为公平锁和非公平锁(默认)两种。ReentrantLock通过state字段判断是否有线程占用了锁,state为0时表示没有其他线程占用了锁,非0时表示已经有线程占用了锁。
当其他线程占用锁时会把未得到锁的线程封装成Node放入FIFO队列中,等其他线程释放锁时,队列中最早加入的线程会获得锁。未得到锁的线程会调用Locksupport.park方法将线程置于waitting状态,唤醒时调用Locksupport.unpark();
lock.lock
公平锁和非公平锁的区别在于lock.lock方法
- 非公平锁在lock.lock时会先判断锁的状态,如果线程A刚释放锁,此时线程B调用了lock.lock方法,此时线程B会直接得到锁。
- 公平锁在lock时则会先判断队列中是否有其他线程在排队,如果有则先让队列中的线程得到锁。
//非公平锁
final void lock() {
//CAS操作试探是否有其他线程占用了锁
if (compareAndSetState(0, 1))
//还没有其他线程占用锁,当前线程占用
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);//已经有其他线程占用了锁,将线程加入等待队列中,里面会判断当前线程是不是已经获得了锁,如果已经有锁将state加1,此处实现了锁的可重入性
}
lock.unlock
调用lock.unlock方法时会把state减1,减1后如果state为0,则从FIFO队列中取出头部的Node(线程),调用Locksupprot.unpark方法把线程唤醒。 当同一个线程调用两次lock.lock方法时,state会递增(调用两次lock则state为2)此时需要调用两次lock.unlock才可将state置0,其他线程才可以竞争锁,这也是锁的可重入性的实现原理。
lock.trylock
lock.lock方法会一直等待其他线程释放锁,有死锁的风险。可以使用tryLock来避免死锁。
lock.trylock返回true则证明得到了锁。也可以指定等待的时间 lock.tryLock(2, TimeUnit.SECONDS) 两秒之内如果得到锁返回true,否则返回false。 tryLock获取锁时finally中调用lock.unlock,如果当前线程没有得到锁就不要调,否则会抛出异常。或者在调用lock.unlock时判断当前线程是否持有了锁。
try {
boolean tryLock = lock.tryLock(2, TimeUnit.SECONDS);
if (tryLock) {
System.out.println("This is The SecondThread");
} else {
System.out.println("Second Thread not get Lock");
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("Second Inturrupt");
} finally {
//如果线程没有得到锁调用lock.unlock会抛出IllegalMoniterException
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
####Condition
Condition作用和wait/notify类似,用来实现线程间调度。Condition通过await()/singal()来挂起和唤醒线程。
condition.await()/singal()跟上面相同也是通过LockSupport.park()/unPark()来实现线程的挂起和唤醒。
使用Condition实现生产者消费者
public ReentrantLock lock = new ReentrantLock();
//数据容器
List<String> list = new LinkedList<>();
//最大数量
final int count = 20;
//condition只能通过lock.newCondition来获得,一个lock可以生成多个condition
Condition consumeCondition = lock.newCondition();//消费者condition
Condition produceCondition = lock.newCondition();// 生产者condition
//生产者
public void put(String str) {
lock.lock();
try {
while (count == list.size()) {
System.out.println("库房满了生产者挂起");
produceCondition.await();
System.out.println("有消费者取东西了生产者被唤醒了");
}
System.out.println("生产者开始生产");
list.add(str);
//通知消费者开始消费
consumeCondition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
//消费者
public String take() {
lock.lock();
try {
while (list.size() == 0) {
System.out.println("仓库空了消费者await");
consumeCondition.await();
}
String s = list.get(0);
list.remove(0);
//通知生产者开始生产
produceCondition.signal();
return s;
} catch (Exception e) {
} finally {
lock.unlock();
}
return "";
}
ReadWriteLock
ReadWriteLock允许多个读线程同时读,写的时候不允许读,内部由独占锁和共享锁实现。独占锁只能被一个线程占有,共享锁则可以由多个线程占有。readLock使用的是共享锁,writeLock使用的是独占锁,ReentrantLock中使用的都是独占锁。
ReadWriteLock内部有readLock和writeLock两把锁。
//writeLock.lock
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
//readLock.lock
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
Synchronized和ReentrantLock使用场景
Synchronized是Java关键字,线程没有获取到锁时会一直阻塞直到获取到锁为止。ReentrantLock可以使用tryLock方法指定超时时间,如果超过了指定时间还没获取到锁则返回false,可以手动处理获取不到锁的场景。
当并发线程数量较少或者同步代码块中任务操作耗时较短时,Synchronized不会膨胀成重量级锁,线程不会挂起,减少了挂起和唤醒操作,而ReentrantLock并发时会直接把线程挂起,再唤醒。所以并发数量较少时Synchronized可以减少上下文切换次数,性能好与ReentrantLock。当并发线程较多时竞争激烈时Synchronized性能较差,而ReentrantLock性能并不会下降太多。