1、线程的实现
-
继承Thread类:在java.lang包中定义,继承Thread类必须重写run()方法,然后通过start()方法去启动线程
如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。 - 实现Runnable接口:通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写run方法。
- 实现Callable接口:执行Callable任务后可以获取一个Future对象,在该对象上调用get就可以获取到Callable任务返回的Object,在结合线程池接口ExecutorService就可以实现有返回值结果的多线程了。
2、线程的状态
- 创建(new):准备好了一个多线程对象;
- 就绪(runnable):调用start方法,等待CPU进行调度;
- 运行(running):执行run方法;
- 阻塞(blocked):暂时停止执行,将资源交给其他线程;
- 终止(dead):线程销毁;
-
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。避免死锁的方式:避免嵌套封锁,如果你已经有了一个资源就要避免封锁另一个资源;只对有请求的进行封锁;避免无限期的等待。
例子:
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
// 得到left锁
synchronized (left) {
// 得到right锁
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
// 得到right锁
synchronized (right) {
// 得到left锁
synchronized (left) {
doSomethingElse();
}
}
}
}
线程A调用leftRight()方法,得到left锁,同时线程B调用rightLeft()方法,得到right锁,线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。但是:线程A的left锁并没有释放,线程B的right锁也没有释放。所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁。
- Time waiting(睡眠或等待一定的事件)
- waiting(等待被唤醒)
- blocked(阻塞)。
3、线程常用方法比较
sleep方法:在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。给其他线程运行机会时不考虑线程的优先级
yield方法:让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。只会给相同优先级或更高优先级的线程以运行的机会
join方法:在很多情况下,主线程创建并启动了线程,如果子线程中要进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。Join不会释放锁。当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
线程中断interrupt:中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。调用interrupt()方法并不会中断一个正在运行的线程。也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。 若调用sleep()而使线程处于TIMED-WATING状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束TIMED-WATING状态。许多声明抛出InterruptedException的方法(如Thread.sleep(long mills方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterrupted()方法将会返回false。中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。
wait方法: JDK在Object对象中提供了2个非常重要的接口线程方法wait方法和notify方法,也就是所有Java对象都有这2个方法,意思就是当在一个实例Java对象上调用wait方法,那么当前线程就会从执行状态转变成等待状态,同时释放在实例对象上的锁,直到其它线程在刚才那个实例对象上调用notify方法并且释放实例对象上的锁,那么刚才那个当前线程才会再次获取实例对象锁并且继续执行。这样我们通过object对象就可以让多线程之间进行有效通信。
比如我们有一个person对象,如果一个线程T1调用person.wait(),那么这个线程a就会进入person对象的等待队列,在这个等待队列中可能还有线程T2,线程T3,线程T4,因为系统可能通过4个线程来等待person实例对象,当我们调用person.notify()方法,它就会从这个等待队列中随机选一个线程,并将其唤醒,在这里这个选择是不公平的,也就是选择线程T1,T2,T3,T4是随机的,当然了也可以调用person,notifyAll()方法,这个方法会把线程T1,T2,T3,T4全部唤醒。线程唤醒(notify):Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
4、线程池 参考1 参考2
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
线程复用:每一个 Thread 的类都有一个 start 方法。 当调用start启动线程时Java虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue, Executors.defaultThreadFactory(), defaultHandler);
}
4.1 线程池参数
- corePoolSize:指定了线程池中的线程数量。
- maximumPoolSize:指定了线程池中的最大线程数量。
- keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多次时间内会被销毁。
- unit:keepAliveTime的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
4.2 拒绝策略
- AbortPolicy : 直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
- DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
- DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
4.3 任务队列
- ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
4.4 线程池类型
- newCachedThreadPool:corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,无限线程池,如果线程池超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程;
- newFixedThreadPool:线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue,创建固定大小的线程池,如果工作线程数量达到线程池初始化的最大数,则提交的任务存入到池队列中;
- newSingleThreadExecutor:corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue,创建单个线程的线程池,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行;
- newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
4.5 执行过程
1). 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2). 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
3). 当一个线程完成任务时,它会从队列中取下一个任务来执行。
4). 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
4.6 线程池参数设置规则
计算密集型的任务比较占cpu,所以一般线程数设置的大小 等于或者略微大于 cpu的核数;但IO型任务主要时间消耗在 IO等待上,cpu压力并不大,所以线程数一般设置较大。
需要根据几个值来决定
- tasks :每秒的任务数,假设为500~1000
- taskcost:每个任务花费时间,假设为0.1s
- responsetime:系统允许容忍的最大响应时间,假设为1s
5、Synchronized
synchronized是Java中的关键字,使用synchronized能够防止多个线程同时进入并访问程序的临界区(程序的某个方法或者代码块)。synchronized可以修饰方法或者代码块,当A线程访问被synchronized修饰的方法或者代码块时,A线程就获取该对象的锁,此时如果B线程想访问该临界区,就必须等待A线程执行完毕并释放该对象的锁。
5.1 synchronized用法
- synchronized method():被synchronized修饰后,该方法就变成了一个同步方法,其作用范围就是整个方法,而作用对象要分两种情况来考虑。
情况一:该方法是非静态方法,其作用对象就是调用该方法的对象;
情况二:该方法是静态方法,其作用对象就是调用该方法的所有类的实例对象。 - synchronized ():括号里可以是类或者对象。
synchronized(className.class):作用对象是访问该代码块的该类所有对象,当某个线程已经在访问该代码块时,其它该类的所有对象都不能访问该代码块。
synchronized(object):是给object加锁,其他线程访问synchronized (object)同步代码块时将会被阻塞(同一个类的不同对象可以访问该代码块)。
synchronized(this):作用对象是当前对象,其他线程访问该对象的同步方法块时将会被阻塞(同一个类的不同对象可以访问该代码块)。
5.2 Synchronized实现参考
1). JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。
2). Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。
3). Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
4). OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
5). 处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
6). Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。 参考
7). 每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的
5.3 原理
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。
其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。
5.4 synchronized锁优化 参考
5.4.1锁对象
java对象在内存中的存储结构主要有三个部分:对象头,实例数据,填充数据
当我们创建一个对象LockObject时,该对象的部分Markword关键数据如下。
当对象创建的时候就有了偏向锁的标志位只是为0还没有生效。
5.4.2 锁膨胀和锁撤销
第一次当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。
当另一个线程来竞争锁的时候,偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。这就是锁膨胀。轻量级锁分为自旋锁、适应性自旋锁。
由于偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费还是挺大的,其大概的过程如下:
在一个安全点停止拥有锁的线程;遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态;唤醒当前线程,将当前锁升级成轻量级锁。如果拥有锁的线程执行很慢,自旋锁一直循环原地等待空消耗CPU,为了避免这个问题加入了循环次数限制,当超过一定次数后轻量级锁膨胀,就升级为重量级锁了。
6、volatile关键字的作用(变量可见性、禁止重排序)
-
可见性
一个变量被声明为volatile时,在编译成会变指令的时候,会多出下面一行:
0x00bbacde: lock add1 $0x0,(%esp);
处理器遇到lock指令时不会再锁住总线,而是会检查数据所在的内存区域,如果该数据是在处理器的内部缓存中,则会锁定此缓存区域,处理完后把缓存写回到主存中,并且会利用缓存一致性协议来保证其他处理器中的缓存数据的一致性。
缓存一致性协议:
线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其他处理器的操作情况,一旦嗅探到某处处理器打算修改其内存地址中的值,而该内存地址刚好也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址的无效。所以当该处理器要访问该数据的时候,由于发现自己缓存的数据无效了,就会去主存中访问。
-
有序性 参考
保证这个变量之前的代码一定比它先执行,但并没有保证这个变量之前的代码不可以重排序。之后的也一样。
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障避免了volatile变量和其它指令重排序、线程之间实现了通信。
对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。AtomicInteger修饰,AtomicInteger原理是CAS。在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。
总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全: (1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。 (2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。
7、原子包java.util.concurrent.atomic
java.util.concurrent.atomic这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) { //CAS自旋,一直尝试,直达成功
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
//CAS操作
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
8、Lock实现原理 参考
AQS是一个同步器有双向链表+int类型的state锁状态值构成。state线程被重入的次数,可能大于0。
- 在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许插队。
可以看到在整个实现过程中,lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用synchronized做同步操作。
synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。Lock是一个接口,源码很简单,主要是声明了四个方法:
//切记Lock不会主动释放锁
public interface Lock {
//用来获取锁,如果锁已被其他线程获取则进行等待,等待过程无法中断。
void lock();
//用来获取锁,如果线程正在等待获取锁则这个线程能够响应中断,即可以中断线程的等待状态。
void lockInterruptibly() throws InterruptedException;
//用来尝试获取锁,如果获取成功则返回true,如果获取失败(即锁已被其他线程获取)则返回false,该方法会立即返回,在拿不到锁时也不会一直在那等待。
boolean tryLock();
//用来尝试获取锁,区别在于拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,如果如果一开始拿到锁或者在等待期间内拿到了锁则返回true,等待期间可被中断。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//用来释放锁,Lock 不会主动释放锁,需要手动调用此方法,一般写在 finally 语句中。
void unlock();
//创建一个 Lock 的 Condition。
Condition newCondition();
8.1 锁的分类
-
偏向锁:
锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,该线程如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作。
a. Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
b. 如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.
c. 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
d. 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。
可以看出,偏向锁是针对于一个线程而言的,线程获得锁之后就不会再有解锁等操作了,这样可以省略很多开销。假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。
-
轻量级锁
轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。分为自旋锁和自适应自旋锁
自旋锁:
指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。
自适应自旋锁:
自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
重量级锁:
互斥锁(重量级锁)也称为阻塞同步、悲观锁。
当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。公平锁/非公平锁:
公平锁和非公平锁是指多个线程是否按照申请锁的顺序来获取锁。-
可重入锁:
线程可以进入任何一个它已经拥有的锁所同步着的代码块。可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。可重入锁的一个好处是可以一定程度避免死锁。
如果不是可重入锁,setB可能不会被当前线程执行,造成死锁。 独享锁/共享锁:
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。互斥锁/读写锁:
独享锁/共享锁是一种广义的说法,互斥锁/读写锁是其具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中具体实现是ReadWriteLock。乐观锁/悲观锁:
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
8.2 CAS
CAS(Compare And Swap/Set)比较并交换,CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量(内存值),E表示预期值(旧的),N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
ABA问题
一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。 部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。
8.3 Lock与synchronized的比较
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。Lock在发生异常时,如果没有主动通过unLock()方法去释放锁,则很可能造成死锁的现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到;
- Lock可以提高多个线程进行读操作的效率。
11、 进程和线程
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
进程线程的区别:
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程 - 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 线程是处理器调度的基本单位,但是进程不是。
- 两者均可并发执行。
优缺点:
- 线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
- 进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。
何时使用多进程,何时使用多线程?
- 对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
- 要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
12、线程之间共享数据
Java里面进行多线程通信的主要方式就是共享内存的方式
-
等待通知机制:两个线程通过对同一对象调用等待 wait() 和通知 notify() 方法来进行通讯。
线程 A 作为消费者,获取对象的锁。
进入 while(判断条件),并调用 wait() 方法。
当条件满足跳出循环执行具体处理逻辑。
线程 B 作为生产者:
获取对象锁。
更改与线程 A 共用的判断条件。
调用 notify() 方法。 - join()方法
- volatile共享内存
- synchronized同步:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了 通信。
13、ThreadLocal(线程本地存储) 参考
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
ThreadLocal的生命周期和当前Thread的生命周期强绑定,正常情况下,在线程退出的时候会将threadLocals这个变量置为null,等待JVM去自动回收。由于现在多线程一般都是由线程池管理,而线程池的线程一般都是复用的,这样会导致线程一直存活,而如果使用ThreadLocal大量存储变量,会使得空间开始膨胀。在ThreadLocal使用结束以后及时调用remove()方法进行清理。
Thread类有一个ThreadLocalMap类型的threadLocals变量。
如上图,当引用ThreadLocal的对象被回收了,因为Map里的key对ThreadLocal为弱引用,那么ThreadLocal也会被回收掉,value在ThreadLocalMap调用get、set、remove的时候就会被清除。
如果为强引用的话引用ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key为强引用并没有被回收,如果不手动回收的话,ThreadLocal将不会回收那么将导致内存泄漏。
当使用线程池的时候线程一直有对map的强引用,那么value一直存在也会导致内存泄漏,所以在使用ThreadLocal时要remove最好。