一、Java并发基础知识
Java里的程序天生就是多线程的,那么有几种新启线程的方式?
两种,启动线程的方式只有:
1、X extends Thread;,然后X.start;
2、X implements Runnable;然后交给Thread运行。
Java 线程的状态
Java中线程的状态分为6种:
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
状态之间的变迁如下图所示:
线程死锁
线程死锁,是指由于两个或者两个以上的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行,死锁不会抛出异常。
死锁的发生必须具备以下条件:
1)互斥条件:指线程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个线程占用。如果此时还有其它线程请求资源,则请求者只能等待,直至占有资源的线程用毕释放。
2)请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它线程占有,此时请求线程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链。
可以简单的理解:
1)M个线程访问(M>=2),访问N个资源(N>=2),同时(N<=M);
2)访问顺序不对,线程A和B都要访问资源C和D,A先访问了C,再访问D;B先访问D,再访问C;A访问完C之后,D被B占用着,只能等待B释放;同时B先访问完D之后,C被A占用着,只能等待A释放;AB都在占用和等待对方,导致死锁;
3)使用资源不释放,如果A访问完C之后就释放C,B访问完D之后就释放D,也不会导致死锁。
死锁代码示例 :
public class NormalDeadLock {
private static Object No13 = new Object();//第一个锁
private static Object No14 = new Object();//第二个锁
//第一个拿锁的方法
private static void jamesDo() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (No13){
System.out.println(threadName+" get nO13");
Thread.sleep(100);
synchronized (No14){
System.out.println(threadName+" get nO14");
}
}
}
//第二个拿锁的方法
private static void lisonDo() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (No13){
System.out.println(threadName+" get nO13");
Thread.sleep(100);
synchronized (No14){
System.out.println(threadName+" get nO14");
}
}
}
//子线程,test2
private static class Test2 extends Thread{
private String name;
public Test2(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
jamesDo();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//主线程,test1
Thread.currentThread().setName("test1");
Test2 test2 = new Test2("test2");
test2.start();
lisonDo();
}
}
死锁危害:
1、线程不工作了,但是整个程序还是活着的;
2、没有任何的异常信息可以供我们检查;
3、一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。
死锁解决方式:
采用尝试拿锁的机制,两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。每个线程休眠随机数,错开拿锁的时间。
尝试拿锁解决死锁示例:
public class TryLock {
private static Lock No13 = new ReentrantLock();//第一个锁
private static Lock No14 = new ReentrantLock();//第二个锁
//先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while(true){
if(No13.tryLock()){
System.out.println(threadName
+" get 13");
try{
if(No14.tryLock()){
try{
System.out.println(threadName
+" get 14");
System.out.println("fisrtToSecond do work------------");
break;
}finally{
No14.unlock();
}
}
}finally {
No13.unlock();
}
}
Thread.sleep(r.nextInt(3));
}
}
//先尝试拿No14锁,再尝试拿No13锁,No13锁没拿到,连同No14锁一起释放掉
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while(true){
if(No14.tryLock()){
System.out.println(threadName
+" get 14");
try{
if(No13.tryLock()){
try{
System.out.println(threadName
+" get 13");
System.out.println("SecondToFisrt do work------------");
break;
}finally{
No13.unlock();
}
}
}finally {
No14.unlock();
}
}
Thread.sleep(r.nextInt(3));
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
public void run(){
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadLocal
ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
ThreadLocal的使用
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
1)void set(Object value)
设置当前线程的线程局部变量的值。
2)public Object get()
该方法返回当前线程所对应的线程局部变量。
3)public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
4)protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
二、CAS基本原理
什么是原子操作?如何实现原子操作?
假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,
这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争,同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
实现原子操作还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。
CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
CAS实现原子操作的三大问题:
ABA问题:
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
循环时间长开销大:
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
Jdk中相关原子操作类的使用:
AtomicInteger :
•int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
•boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
•int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
•int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
AtomicIntegerArray:
主要是提供原子的方式更新数组里的整型,其常用方法如下。
•int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
•boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。
更新引用类型:
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。
AtomicReference:原子更新引用类型。
AtomicStampedReference:利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。 还是那个水的例子,AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。