多线程
线程与进程的区别
学习多线程首先要搞清楚线程和进程之间的区别,主要区别如下:
进程
进程是程序的一次执行过程,是一个动态的概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间。
进程至少有五种状态:初始态,执行态,等待态,就绪态,终止态
线程
CPU调度和分派的基本单位。
区别
1、一个线程只能属于一个进程,而一个进程可以拥有多个线程。
2、进程是一段正在运行的程序,而线程只是CPU调度的基本单元。
3、进程之间相互独立,不能共享资源,一个进程至少拥有一条线程,同一进程下的线程共享整个进程资源(包括寄存器、上下文、堆栈)
4、线程的开销比进程小;
线程状态
多线程中的状态:
1、新生态:表示线程被实例化完成,可以去争夺CPU时间片
2、就绪态:表示被实例的线程正在争抢CPU时间片
3、运行态:表示线程已经抢到了CPU时间片,在运行线程中的逻辑
4、阻塞态:表示线程中的逻辑因为其他原因停止运行,交还出了CPU时间片
5、死亡态:表示线程结束
6、锁池:线程锁存在的地方
运行过程
首先,线程由新生态进行创建,由调用start方法进入到就绪态开始争夺CPU时间片,当争夺到了CPU时间片,线程由就绪态进入到运行态,开始运行线程中的逻辑,当发生用户输入(这里的输入是广义的输入,不仅限于键盘输入)、调用了sleep方法、join方法(合并线程)时,线程进入到阻塞态,当阻塞结束,会回到就绪态。这里注意yield方法(线程礼让),礼让的是CPU时间片,就是当调用了yield方法时,会将当前持有的CPU时间片归还,当前线程与其他线程一起重新争夺CPU时间片,也就是说,调用yeild方法后,当前线程也是有概率会重新拿到CPU时间片的。
当线程调用wait方法时,线程会进入到等待队列,此时的线程不会被自动重新执行,需要调用notify或notifyAll方法后,才会重新被执行,这个重新执行,也是重新进入到就绪态,去争夺CPU时间片。
线程运行结束后,进入死亡态,被GC回收
针对问题
线程锁主要针对的问题是临界资源问题,也就是当有一块资源,几条线程都可以对其进行操作的时候,一些特定的逻辑中,就需要对线程加锁,当上锁后,一条线程拿到线程锁,其余的线程就要进入到锁池去等待,等待当前运行中的线程释放线程锁后,在锁池中的线程才能进入到就绪态去争夺CPU时间片,进而可以运行。
加锁的方式
锁分为对象锁、类锁,需要注意,多个线程看到的锁,需要是同一把锁才行,锁的其实是对象,也就是锁实际上加在堆内存上而不是栈内存上,当对静态方法加锁时,锁的是当前方法的类锁,也就是当前类.class,非静态方法的锁锁的就是当前对象也就是this(因为静态方法是随着类的启动而启动的,优先级上面是在对象之前的,使用的时候也是可以直接使用,所以没有经过实例化的过程,加载是跟随类,所以通过.class方式来加载其对象)
死锁
死锁就是多个线程彼此持有对方的锁对象,就会造成死锁现象
例如:
Runnable runnable = ()->{
synchronized("aa"){
System.out.println("aa线程持有了aa锁,等待bb锁");
try{
"aa".wait();
}catch(InterruptedException e){
e.printStackTrace();
}
synchronized("bb"){
System.out.println("aa线程同时持有了aa和bb")
}
}
};
Runnable runnable = ()->{
synchronized("bb"){
System.out.println("bb线程持有了bb锁,等待aa锁");
synchronized("aa"){
System.out.println("bb线程同时持有了aa和bb两把锁");
}
}
};
线程重入
当一条线程持有了锁,其它线程想请求这把锁的时候,会发生是阻塞,但是当这条线程申请自己已经持有的锁的时候,那么这个请求就会成功,重入意味着获取锁这个操作的粒度是线程,而不是调用;重入的一种实现方法是给锁加上计数器,每次被持有都+1,当被释放的时候计数器为0,当线程请求一个未被持有的锁的时候,JVM记下锁的持有者,并将此锁的计数器加一,如果同一个线程再次获得该锁,继续加一(递增),每次线程释放(退出同步代码块),计数器递减,当计数器为0的时候,锁被释放,适用于子类继承父类,这个时候会先调用父类的方法,也就是父类首先获得锁以后,子类继承,子类的方法就可以获取到父类持有的这把锁。
wait、notify、notifyAll
wait方法可以打断一个正在运行的线程,使当前线程进入到等待池中等待唤醒,且等待池中的线程不会参与竞争该对象的锁,wait方法与线程的sleep方法不同,主要区别在于:
1、wait是作用与加锁的对象上面,而sleep作用于一条线程。(wait方法定义在Object类中,sleep方法定义在java.lang.Thread中)
2、sleep方法可以通过interrupt()方法或者超时来唤醒。
3、wait方法只能在同步方法中调用,不然抛出IllegalMonitorStateException异常。
4、wait方法是实例方法,sleep方法是静态方法。
使用wait方法后,单签对象会释放所持有的锁进入等待区,释放掉的锁可以被其他对象持有,唤醒wait方法可以使用notify或notifyAll方法,两者区别在于一个会随机唤醒一个等待池中的线程,notifyAll会唤醒所有等待吃中的线程,线程唤醒后会进入到就绪态(注意,唤醒后不是直接拿到锁,而是去其他线程一起去争夺)。
notify、notifyAll都不可以指定唤醒的线程是哪一条
CountDownLatch、CyclicBarrier、semaphore
CountDownLatch
CountDownLatch是java.util.cocurrunt包下面的一个方法,也就是门闩方法,是一个类似于计数器的方法,就是执行过i条线程后,等待
CountDownLatch只提供了一个构造器:
public CountDownLatch(int i){};//i是计数器的计数值
CountDownLatch中主要的方法:
public void await throws InterruptedException{};//调用await的方法会被挂起,直到i为0才能继续执行。
public boolean await(long timeout,TimeUnit) throws InterruptedException{};//和await类似,不同点在于有一个超时时间,到了时间之后,就算i不为0,也会继续执行
public void countDown{};//将i值减一
下面的例子可以很直观的看出CountDownLatch的用法(转载自:CountDownLatch、CyclicBarrier、Semaphore的区别)
package code;
import java.util.*;
import java.util.concurrent.CountDownLatch;
public class tesy{
public static void main(String[] args){
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
public void run(){
try{
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}.start();
new Thread(){
public void run(){
try{
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}.start();
try{
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个线程已经执行完毕");
System.out.println("继续执行主线程");
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
执行结果:
CyclicBarrier
CylicBarier方法,字面意思是回环栅栏,通过它可以让一组线程等待到一个状态后,再一块执行,,回环是因为CylicBarier可以重用,CylicBarier也位于java.util.concurrunt包下,主要构造方法有两个:
public CyclicBarrier (int parties,Runnable barrierAction);
public CyclicBarrer(int parties);
参数parties表示让多少个任务或者线程等待,参数barrierAction表示运行到了相应的线程或任务之后要执行什么操作,CyclicBarrer中最重要的就是await方法
public int await() throws InterruptException,BrokenBarrierException{};
public int await(long timeout,TimeUnit timeunit) throws InterruptException,BrokenBarrierException,TimeoutException{};
第一个await就是将线程暂时挂起,等到多有线程都到达屏障后,唤醒所有线程
第二个是等待一定时间,时间到了之后,无论后面的线程或者任务有没有到达屏障,都把先前到达屏障的线程或者任务执行;
重要的属性:
/**
* Generation一代的意思。
* CyclicBarrier是可以循环使用的,用它来标志本代和下一代。
* broken:表示本代是不是损坏了。标志有线程发生了中断,或者异常,就是任务没有完成。
*/
private static class Generation {
boolean broken = false;
}
/** 用它来实现独占锁 */
private final ReentrantLock lock = new ReentrantLock();
/** 用它来实现多个线程之间相互等待通知,就是满足某些条件之后,线程才能执行,否则就等待 */
private final Condition trip = lock.newCondition();
/** 初始化时屏障数量 */
private final int parties;
/* 当条件满足(即屏障数量为0)之后,会回调这个Runnable */
private final Runnable barrierCommand;
/** 当前代 */
private Generation generation = new Generation();
// 剩余的屏障数量count。当count==0时,表示条件都满足了
private int count;
1、count:表示单签剩余的屏障数量。
2、generation:当前代
3、lock:独占锁,用它来保证修改成员变量的时候的高并发问题(保证同一时间只能有一条线程可以修改)
4、trip: Condition对象,用它来实现不满足条件时,线程等待,满足条件时,唤醒等待线程。
示例:
CyclicBarrier是可以复用的,其中的generation表示的是代,这个代,就是说一批线程,就是一代
public class CyclicBarrierTest {
public static void newThread(String name, CyclicBarrier barrier) {
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程"+Thread.currentThread().getName()+"等待船满过河");
barrier.await();
System.out.println("线程"+Thread.currentThread().getName()+"过完河, 各自行动");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}, name).start();
}
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(10, new Runnable() {
@Override
public void run() {
System.out.println("\n在线程"+Thread.currentThread().getName()+"中 船过河了\n");
}
});
for (int i = 1; i <= 10; i++) {
newThread("t"+i, barrier);
}
}
}
运行结果:
线程t1等待船满过河
线程t2等待船满过河
线程t3等待船满过河
线程t4等待船满过河
线程t5等待船满过河
线程t6等待船满过河
线程t7等待船满过河
线程t8等待船满过河
线程t9等待船满过河
线程t10等待船满过河
在线程t10中 船过河了
线程t10过完河, 各自行动
线程t1过完河, 各自行动
线程t2过完河, 各自行动
线程t3过完河, 各自行动
线程t4过完河, 各自行动
线程t5过完河, 各自行动
线程t6过完河, 各自行动
线程t7过完河, 各自行动
线程t8过完河, 各自行动
线程t9过完河, 各自行动
package code;
import java.util.*;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
public class tesy{
public static void main(String[] args){
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for(int i=0;i<N;i++)
new Writer(barrier).start();
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier){
this.cyclicBarrier = cyclicBarrier;
}
public void run(){
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try{
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
}catch(InterruptedException e){
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
运行结果:
实际上CyclicBarrier就是一个栅栏,意思是等待其他线程都到这里了之后,再一起走
semaphore
semaphore字面意思就是信号量,实际上就是synchronized的加强,两者都是控制并发,但是semaphore是可以控制同一时间同事运行的线程数,这点是synchronized所无法完成的。
Semaphore也是位于java.util.concurrunt包下,提供了两个构造器:
public Semaphore(int permits){//permits表示许可数目,就是同时允许多少条线程访问
sync = new NonFairSync(permits);
}
public Semaphore(int permits,boolean fair){//permits同上,fair表示是否公平,就是获得锁的顺序是否与先后顺序有关,先启动的线程先获得锁,但是只是大概的,不是100%的
sync = (fair)?new NonFairSync(permits):new NonFairSync(permits);
}
acquire():用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
release():用来释放许可。注意,在释放许可之前,必须先获得许可。
许可的获得和归还通过acquire()和release();实现,方法开始的时候,先new一个Semaphore(i),i表示初始化的通路是多少
方法acquire(int permits):
这个方法中参数的意思是每次线程通过所占用的通路数量是多少,例如:
Semaphore sm = new Semaphore(8);//这里的意思是允许8个通路通过,注意这里的8是通路,不是线程
sm.acquire(2);//这句表示当前线程进入,占用两条通路
//假设这里有一些逻辑
sm.release(2);//表示结束归还了两条通路
注意:如果占用的通路一直大于归还的通路的话,通路会被用光,如果线程数多,会造成线程堆积,如果占用一直小于归还,通路不会被用光,会运行的越来越快,但是也有可能出问题
实际应用过程中,acquire()和acquire(int permits)方法会抛出InterruptException,如果在acquire和release中间的代码是一段比较慢的复制的运算,占用内存过多,或者栈深度很深等情况,jvm会出于自我保护,将这段代码中断,如果不想让这段代码中断,可以选择使用acquireUninterruptibly()来替换acquire(),使用acquireUninterruptibly(int permits)来替换acquire(int permits),这样就不会抛出异常,代码可以一直执行下去,不过使用的时候需要慎重。
使用的时候使用try/catch/finally,try中占用通路,catch抛出Interruptbily异常,finally来归还通路
其他工具方法
1、availablepermits(),用来查看当前可用通路数量(可用许可数量,但是数量是一直在动态变化的)
2、drainPermits(),用来立即获得所有可用的通路(许可)的个数,并将其(可用许可)置为0
3、getQueueLength(),获得等待中的线程的个数
4、hasQueueThreads(),Boolean,判断是否有线程在等待许可
如果不想被阻塞,立刻得到结果,这些方法是Acquire的扩展版:
public boolean tryAcquire(){};//尝试获取一个通路(许可),成功立刻返回true,失败返回false
public boolean tryAcquire(long timeout,TimeUnit timeunit) throws InterruptedException{};//尝试获取一个许可,在指定时间内成功的话立刻返回true,否则返回false
public boolean tryAcquire(int permits){};//尝试获取permits个通路,如果成功,立刻返回true,否则返回false
public boolean tryAcquire(int permits,long timeout,TimeUnit timunit)throws InterruptedException{};//尝试获取permits个通路,如果在指定时间内成功,立刻返回true,否则返回false
多通路多处理-多通路单处理
如果有semaphore在逻辑内的时候,需要分别对每层的逻辑实现通络,才能保证不会一次性所有线程同时运行
mutex
互斥:在java中没有mutex的API,当semaphore被初始化完成的时候,如果后面没有加通道数量,那么信号量默认为1,因为在semaphore中没有所有权的概念,简单说mutex是排他的,但是semaphore也有排他性,但是其可以定义多个可以获取资源的对象。
monitor
管程,也常常被翻译成监视器,在使用mutex和semaphore的时候,需要非常小心的控制up和down的操作,为了更好的编程,所以出现了更加高级的原语monitor,monitor实现模式实际上是编程语言在语法上提供的语法糖,实现monitor机制实际是由编译器来完成的,java就是这么做的
monitor的重要特点是同一时刻只能有一条线程能进入到monitor所定义的临界区,这样monitor就有了互斥的效果。
实际上monitor是一个对象,对象的所有方法都被互斥的执行,有点类似于许可,任何一个线程进入任何一个方法都要获得许可,离开的时候将许可归还,还提供singal机制,就是持有许可的线程可以暂时放弃许可,等某个条件达成(谓词成真)后,再通知这个线程可以去重新获得许可。
比较
CountDownLatch和cyclicbarrier都是为了实现线程间的等待,但是等待的侧重点不同,CountDownLatch是每运行一个线程计数器减一,CyclicBarrier是加一;
CyclicBarrier的栅栏可以复用;
CyclicBarrier是等待其他线程之行好了之后,一起执行后面操作,CountDownLatch是等其它线程执行了之后,再执行当前逻辑。
semaphore有点类似于控制访问数量(synchronized升级版)
注意
加锁保证了线程的原子性(就是不能被重入),原因是被synchronized修饰的方法只有拿到线程锁才能够继续执行,没有修饰的则不需要拿到锁,也就是没有任何的规则,可以任意执行。
业务代码中,如果只对读加锁,不对写加锁,很容易出现脏读现象,
给静态对象加锁时,需要加锁的对象是当前对象.class,因为静态对象在被调用的时候不需要被实例化,没有经过new的过程,也就不会产生this这个对象。
synchronized锁是可重入锁,一个已经拥有锁的对象可以调用另一个拥有锁的对象(一个同步方法可以调用另一个同步方法),一个线程已经拥有某个对象的锁,再次申请的时候仍然会拥有该对象的锁,通俗来说,就是这个线程在运行的时候,在线程开始与结束之间,可以运行另一个同步对象。
加锁的不能是字符串常量!
子类可以调用父类的同步方法(子类对象在调用的时候先调用父类对象)
共享资源同享的时候,需要资源同步
在程序运行的过程中,如果出现异常,默认情况下,锁会被直接释放掉,所以在并发处理的过程中,要小心处理异常,例如在一个webapp的处理过程中,多个servlet共同访问同一个资源(临界资源),这时候如果异常处理的不合适,第一个线程抛出异常,后面的线程会进入同步代码区访问临界资源,有可能会访问到错误的数据,也就是第一个线程的数据可能只处理了一半或并未开始处理,就被其他线程拿到锁,继续处理。
如果锁定某个对象,对象的值发生改变的时候,不影响锁的使用,如果变成另外一个对象,锁定的对象就会被改变,应尽量避免;如:已经加锁的对象在后面的使用中又进行了一次实例化,相当于其在堆内存中的锁的指向就变了。
锁是且只能锁在堆内存中new出来的对象上面,不是锁在栈里面对象的引用上面
volatile关键字
volatile关键字用来处理线程之间的可见性问题,在JMM(java内存模型java memory module)中,有一块公用的大的内存区域(主内存),线程实际运行的时候占用的是CPU(线程的工作区),CPU中都有一块缓冲区,在线程运行之前,如第一条线程拿到了锁,会将一些临界资源从主内存读取到缓冲区中,进而进行处理,后面的运行过程中,不会重复的去主内存中去读取,当有其他线程对变量进行修改,第一条线程也不会去读取,加了volatile关键字之后,在对主内存进行修改的时候,会对其他线程进行更新通知(缓存过期提醒)
注意
线程间的通信永远都是通过共享内存来实现的,volatile只能保证线程之间的可见性,不能代替synchronized来保证线程之间的同步。
volatile和synchronized两个特点
首先需要了解线程安全的两个特点,执行控制和内存可见
执行控制的目的是控制代码执行顺序以及是否可以并发执行。
内存可见控制的是线程执行的结果在内存中对其他线程的可见性。根据java内存模型的实现,线程在具体执行的时候,会优先拷贝到线程本地内存(CPU缓存),操作完成后也就是线程执行结束后,再从本地缓存刷到主内存。
synchronized解决的是执行控制问题,他会阻止其他线程得到当前对象的监控锁,这样就使得当前代码中被synchronized保护的对象或代码块无法被其他线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷新到主内存中,从而保证了操作内存的可见性,同时也使先获得这个锁的所有操作都happens-before于随后获得这个锁的所有操作。
volatile关键字解决的是内存可见性的问题,加了volatile关键字后,会使所有的对volatile变量进行的操作都直接刷到主内存中,即保证了变量的可见性。这样就能满足一些对变量的可见性有要求但是对读取顺序没有要求的需求。
注意
使用volatile关键字只能实现对原始变量操作的原子性,要特别注意volatile不能保证复核操作的原子性,如i++这个操作,实际也是多个原子操作组合而成的(read、inc、write),假如多个线程同时执行I++,volatile只能保证操作的i是同一块内存,但是依然可能会出现脏读的现象。
在java5提供了原子数据类型atomic wrapper class,对他们之间的increase之类的都是原子操作,是需要使用synchronized关键字
如AtomicInteger
对于volatile关键字,满足以下条件就可以用
1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
2、该变量没有包含在其他变量的不等式中。
volatile和synchronized的区别
volatile的本质是告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized是锁定当前变量,只有当前线程可以访问当前变量,其他想要访问的变量被阻塞住。
volatile只能使用在变量级别,synchronized可以使用在方法、变量、和类级别。
volatile只能实现变量的修改可见性,不能保证变量的原子性;synchronized可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞,但是synchronized可能会造成线程阻塞。
volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化
java中原子操作的简单类型
AtomicXXX
第一列 | 第二列 | 第三列 |
---|---|---|
原子性的int | AtomicInteger | AtomicInteger count = new AtomicInteger(0); |
AtomicInteger count = new AtomicInteger(0);
/*synchronized*/void m(){ //此处不需要加synchronized就可以保证原子性
for(int i=0;i<10000;i++)
count.incrementAndGet();//这句方法具备原子性,所以不会被打断
}
这个实现是要比synchronized更底层一些,所以效率会高很多。
但是如果用这个修饰多个方法的话,如果方法上面没有加synchronized修饰,还是会被打断,破坏原子性
//举例:
for(int i=0;i<10000;i++)
if(count.get()<1000){//这句有原子性
count.incrementAndGet();//这句有原子性
}
}
//虽然两句都有原子性,但是两句之间还是有可能被其他线程插入。
synchronized优化
被synchronized修饰的东西尽量越少越好,也就是说代码块中的语句越少越好,锁定的东西越少,执行效率越高(细粒度的锁的执行效率要高于粗粒度的锁)
(未完待续)