进程相对于程序来说是一个动态的概念,如:QQ、微信等是一个程序,当双击程序时,程序运行起来就是一个进程。线程作为进程中一个最小的执行单元,线程(Thread)就是一个程序中不同的执行路径,用如下代码对线程进行说明:
public class ThreadPractise {
private static class T1 extends Thread {
@Override
public void run() {
for (int i=0;i<10;i++){
try {
TimeUnit.MICROSECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(“T1线程”);
}
}
}
public static void main(String[] args) {
//启动线程的方式1
new T1().run();
//启动线程的方式2
new T1().start();
for(int i=0;i<10;i++){
try {
TimeUnit.MICROSECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main线程");
}
}
}
run方式:直接调用重写后的run方法,运行结果如下:
可以看到,程序启动时,先调用T1的run方法,执行完该方法之后,然后执行了main方法中的输出内容。run方法的调用相当于main方法开始执行时,先执行T1的run方法,执行完之后再回到main方法,此时程序中只有一条执行路径。
start方式:调用父类Thread的start方法,运行结果如下:
可以看到,输出的结果为T1的方法和main方法交替输出。原因是main方法执行时,当调用start方法时,会产生一个分支,这个分支会和主程序一起运行,这就是一个程序中不同的执行路径,不同的线程在同时运行。
通过如下代码进行说明:
注意:1)调用的是start方法,不是run方法!2)通过实现Runnable接口来创建线程时,要想让线程运行起来,必须先new Thread,然后将实现Runnable接口的类传进去,然后调用start方法。
面试题:启动线程的三种方式。
解答:1)通过继承Thread;2)实现Runnable;3)通过Executors.newCachedThreadPool()拿到线程池,然后启动线程,在线程池中启动线程其实用的也是前两种方式。
1.sleep:当前线程暂停一段时间,让别的线程来运行。从操作系统角度来讲,CPU中是没有线程的概念的,其内部是不断的死循环,从内存中取指令执行,一直循环,没有指令的话,就歇着,所以对CPU来说,是没有线程的概念的。而多线程是指,如果只有一个CPU的话,有许多不同的线程,每个线程在CPU上执行一会,执行完将当前线程扔出去,将下一个线程哪进来。
2.yield:A线程在运行,B线程也在运行,A线程在CPU上运行时调用了yield方法,然后它会谦让的先离开CPU,此时别的线程可以在CPU上执行,当然如果没有线程执行,A还是会再回来执行的,所谓的离开就是进入到一个等待队列中。
3.join:加入的意思。如果有T1、T2俩线程,如果是在T1的某个点上调用了T2.join(说明:在自己的线程中调用join是没有意义的),此操作的意思是跑到T2上去运行,T1在等着,什么时候T2运行完了,再运行T1,相当于把T2线程加入到T1线程中。join方法经常用来等待另外一个线程的结束。
关于join方法的面试题:现有三个线程:T1、T2、T3,如何才能保证三个线程按顺序执行完。
解答:1)主线程起来之后,先调用T1.join,再2调用T2.join,最后调用T3.join;2)在T1线程中调用T2.join,T2线程中调用T3.join,保证T3先执行完,然后是T2,最后是T1。
如下图所示:
new状态:当new Thread后,但是还没有调用start方法时,就是new状态。
Runnable状态:当调用start时,会被线程调度器来执行(也就是交给操作系统来执行),操作系统执行时,整个状态是Runnable,内部有两个状态:Ready(就绪状态)和Running(运行状态),就绪状态是指扔到CPU的等待队列中,真正在CPU上运行时是Running状态。
Terminated状态:当一切顺利执行完之后,进入此状态。注意:当执行完Terminated时,不可以再调用start方法,这是不被允许的!
以下是Runnable状态的变迁:
TimedWaiting状态:按照时长来等待,在运行时,如果调用了Object的wait(time)方法/Thread的join(time)方法/LockSupport 的parkNanos()方法/LockSupport的parkUtil()()方法,进入了TimedWaiting状态,超过设置时间,自动从阻塞状态回到Runnable状态。
Waiting状态:在运行时,如果调用了Object的wait方法/Thread的join方法/LockSupport的park方法,进入了Waiting状态,通过Object的notify方法/Object的notifyAll方法方法/LockSupport的unpark方法重新回到Runnable状态。
Blocked阻塞状态:加上同步代码块,进入代码块中没有得到锁的时候,进入阻塞状态。获得锁的时候,进入就绪状态,去运行。
说明:1)这些状态都是有JVM管理的,JVM管理时要通过操作系统,JVM是跑在操作系统上的一个普通的程序。
2)可以通过调用线程的getState()方法来查看线程的状态。
当我们调用Object的wait方法/Thread的join方法/LockSupport的park方法时都有可能被interrupt(打断),被打断之后会抛出InterruptException,需要在原来的程序中catch InterruptException异常,这里需要注意的是:并不是依赖interrupt方法之后,程序就被打断了,而是当你catch到这个异常后具体的处理,如果你决定停止,那程序就停止;如果你决定catch到异常后该干嘛干嘛,那程序继续运行。
stop方法:在工程中,该方法已经被废弃,不建议使用。
interrupt方法:在框架源码用来控制程序的业务逻辑流程,Netty源码和JDK锁的源码用到过。我们写代码时一般很少用,当我们起了一个等待时间非常长甚至是阻塞状态的操作,如:读网络上传来的一个包,包不来就一直停着。这时我们想关闭整个程序,如何通知这个正在等待的线程呢?如果线程处于wait,可以通过notify来通知正在等待的线程;如果是线程是sleep状态,调用interrupt来唤醒线程,此时需要catch InterruptException。
synchronized关键字既保证了多个线程操作时的原子性,又保证了可见性,但是不能防止指令重排序(下面会介绍)。用下图来说明为什么多个线程访问同一个资源时,需要上锁:
误区:我们使用synchronized的时候,到底是对谁进行了锁定?
如下所示:
注意:这里不是对非得要访问的对象进行锁定,是你想锁谁就可以锁谁,只是当你要执行相应代码时,先要拿到锁对象。如:
public class SynchronizedTest {
public class T {
private int count = 10;
Object o = new Object();//这里锁的是Object对象
public void m(){
synchronized (o){//任何线程要想执行下面代码,必须先要拿到o的锁
count--;
System.out.println(Thread.currentThread().getName()+"count"+count);
}
}
}
}
注意:给某个对象上锁时,该对象不能用String常量、Integer、Long等数据类型!
分析:所有用在字符串常量的地方,其实都是用的同一个(String底层是用final修饰的),假如第一个线程锁定了字符串常量,第二个线程又尝试锁字符串常量,其实第二个线程锁的跟第一个线程是同一个对象,另外如果这俩线程是同一个,那就重入了,但是重入之后不一定是你想要的结果,如果不是同一个线程,就可能造成死锁。而Integer内部进行了处理,当new出来的Integer对象只要值改变了,它就会产生新对象,此时锁对象就改变了。
如果每次加锁的时候都要new一个对象的话,太麻烦了,所以可以用synchronized(this)
来锁定当前对象,也可以在方法上加synchronized,这两种方式是等值的。如:
```java
public class SynchronizedTest1 {
public class T {
private int count = 10;
//方式一:方法上加锁
public synchronized void m(){
count--;
System.out.println(Thread.currentThread().getName()+"count"+count);
}
//方式二:锁this
public void n(){
synchronized (this){
count--;
System.out.println(Thread.currentThread().getName()+"count"+count);
}
}
}
}
如果是被static修饰的静态资源,是没有this对象的,那么加了锁后,它锁的是谁呢?用如下代码来解释:
```java
public class T {
private static int count = 10;
public synchronized static void m(){//这里等同于synchronized(T.class)
count--;
System.out.println(Thread.currentThread().getName()+"count"+count);
}
}
每一个class文件load到内存中,会生成一个class类的字节码对象,和load到内存的代码相对应。这里synchronized(T.class)锁的就是T的class字节码对象。
思考1:这里T.class在load到内存中是不是单例的呢?
结论分析:一般情况下是单例的。如果是在同一个classLoad(类加载器)空间中,就是单例的,如果load到不同的classLoad空间中,就不是单例,同一个进程中可以有多个classLoad,classLoad是可以自定义的,但是不同的classLoad互相是不能访问的,所以只要能访问,那一定是单例的。
思考2:锁定的必须是同一个对象吗?
结论分析:是的,必须是同一个对象。如果两次锁的对象不同,那么这两个对象不能构成互斥,不是互斥的锁是没有意义的。
面试题:模拟银行账户操作业务
前提:该银行允许用户出现脏读(就是在还没有set完的时候读取数据)
要求:对业务写方法进行加锁;对业务读方法不加锁
可以参考如下代码:
public class Account {
private String name;
private double balance;
public synchronized void set(String name,double balance){
this.name = name;
//模拟场景:写方法sleep,读方法执行
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
this.balance = balance;
}
public /*synchronized*/ double get(String name){
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
//启动线程
new Thread(()->a.set("zhangsan",100.00)).start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
//写方法sleep的时候,用户读数据,因为set方法睡了2秒钟,读线程睡了1秒后得到的数据是还没有set完的数据
System.out.println(a.get("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(a.get("zhangsan"));
}
}
结论:锁定方法(同步方法)和非锁定方法(非同步方法)可以同时执行。
可重入:同一个线程,如果一个同步方法调另一个同步方法,如:方法m1加了锁,另一个方法m2也加了锁,而且加的是同一把锁,此时m1方法中调用m2,同样m2方法也调了m1,如果synchronized是不可重入的,那么m1中调m2时,就会发生死锁,因为不是可重入的话,是不允许访问的,方法进行不下去。如果是可重入的,执行到m2方法时,会发现它加的锁跟m1自己的一样,就会去执行m1方法。还有一个例子:如果一个子类重写了父类的某个同步(加了锁)方法,在重写的方法中调用super.同步方法,如果synchronized是不可重入的,那么在父子类之间就发生了死锁。
注意:1)程序执行过程中,如果出现异常,默认情况下锁会被释放。
2)在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如:在一个web app处理过程中,多个业务层的线程共同访问一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。
为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,其语法如下:
synchronized(要锁定的对象){
// 同步代码块
}
其中obj就是同步监视器,它的含义是:线程开始执行同步代码块之前,必须先获得对同步代码块的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。虽然java程序允许使用任何对象作为同步监视器,但是同步监视器的目的就是为了阻止多个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
锁升级的概念:早期的synchronized是重量级的(就是需要向操作系统申请锁),到JDK1.5之后,对synchronized进行了改进,后来当我们使用synchronized的时候,Hotspot(Oracle对JVM规范的实现)的实现是:当我们访问上了锁的线程时(如:sync(Object)),底层实际上没有直接加锁,而是先在Objeect “头上” markword(JVM中的知识点) 做个标记,表示这个线程的ID(偏向锁),如果下次还是这个线程访问,直接进入,这样效率非常高,这种情况下通常是一个线程在执行;如果不是这个线程,此时就会有线程的争用,这时候偏向锁升级为自旋锁(也叫乐观锁,就是别的线程在一旁等着锁释放),自旋锁默认旋10次(次数可以设置);如果自旋10次之后还得不到锁,此时升级为重量级锁(就是去向操作系统申请锁)。
注意:1)Hotspot实现中,锁只能升级,不能降级。
2)锁自旋的过程中,会消耗CPU资源,如果此时别的线程一直释放不了锁,再去申请为重量级锁的时候,这个线程就进入等待状态,从而进入了等待队列,此时就不再占用CPU了。
问题:什么情况下应该使用自旋锁?
分析:自旋锁是在用户态解决锁竞争的问题,不经过内核态(就是访问操作系统),因此在加锁解锁的效率上比重量级锁高。所以在加锁代码执行时间长、并且线程数比较多,尽量使用重量级锁,就是synchronized锁;加锁代码执行时间短、并且线程数比较少,使用自旋锁。
1.保证线程可见性
以如下代码来进行说明:
public class HelloVolatile {
/*volatile*/ boolean running = true;
public void m(){
System.out.println(Thread.currentThread().getName()+" start");
while (running){
}
System.out.println(Thread.currentThread().getName()+" end");
}
public static void main(String[] args) {
HelloVolatile v = new HelloVolatile();
//下面这种写法是lambda表达式写法,相当于:new Thread(new Runnable( run(){ m() })).start();
new Thread(v::m,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
v.running = false;
}
}
说明:当running不加volatile时,程序会一直运行,不会输出 “t1 end”,加了volatile之后,就会输出 “t1 end”。
原理说明:Java中的堆内存是所有线程共享的内存,并且每个线程都有自己的工作空间。该程序中,共享内存中running变量值为true,此时有两个线程(main线程、t1线程)来访问,它们会将变量值copy到自己的工作空间中,然而此时在main线程将值改为false,然后写到共享内存中,此时改过的值并没有及时反映到 t1 的工作空间中,t1 线程中就不会输出 “t1 end”,这就是线程之间的不可见。对变量值加volatile之后,就能够保证main线程修改值之后,t1 线程就会看到,就会输出 “t1 end”。
volatile:本质是使用了CPU的缓存一致性协议,由于多个线程运行在不同的CPU上,所以多个CPU之间会有缓存(CPU本身也有缓存,也就是所谓的三级缓存)。Java中线程之间的可见性,要靠CPU的缓存一致性协议才能保证。
2.volatile作用:禁止指令重排序
指令重排序:现在的CPU为了提高效率,执行指令时会并发的执行,如:第一个指令执行时,第二个可能已经开始执行了,就是流水线式的执行。在现在CPU架构的基础之上,要充分利用并发执行的效率,要求在编译器将原码编译完的指令之后,可能要进行指令的重排序,细节上就是汇编语言的重排序。先来一波小插曲,然后逐渐引入重排序问题。
复习:单例模式—保证在JVM内存中,永远只有某个类的一个实例。以如下代码进行说明:
public class SingleMode {
//单例模式—饿汉式
//饿汉式不足:不管你用不用对象,在创建类实例的时候就会初始化,浪费内存
private static final SingleMode INSTANCE = new SingleMode();
//私有化构造方法,不让外界创建对象,只能自己创建
private SingleMode(){};
//设置外界访问本类实例的唯一访问点,通过getInstance()方法
public static SingleMode getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
//这里创建了两个SingleMode实例,如果输出为true,说明两个实例是同一个对象
SingleMode s1 = SingleMode.getInstance();
SingleMode s2 = SingleMode.getInstance();
System.out.println(s1==s2);
}
}
```java
public class SingleMode2 {
//单例模式—懒汉式
private static SingleMode2 INSTANCE;
//私有化构造方法,不让外界创建对象,只能自己创建
private SingleMode2(){};
//设置外界访问对象的方式,通过getInstance()方法访问
public static SingleMode2 getInstance(){
if(INSTANCE==null){
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new SingleMode2();
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(SingleMode2.getInstance().hashCode());
}).start();
}
}
}
此时虽然解决了多会用多会初始化的问题,但是多个线程访问的时候还是会有问题,如:现有T1、T2两个线程,T1线程判断INSTANCE为空,然后创建对象,此时好巧不巧,T2线程也判断INSTANCE为空,也创建了对象,此时就不是单例了。要解决这个问题,可以加synchronized,但是此时需要考虑在方法上加锁是否合理?因为我们加锁的时候要尽量保证锁的代码越少越好,因此双重校验就产生了,代码上加锁之前先判断INSTANCE是否为空,为空的话再初始化,但是多个线程同时访问的话又会有问题,当第一个线程判断INSTANCE为空之后,去初始化对象,此时另一个线程也判断为空(因为此时线程间不可见),然后又会初始化一次。此时volatile就派上用场了,因为volatile可以保证线程间的可见性,当前一个线程初始化完之后,下一个线程" 知道 "已经初始化过了,就不会再次初始化了。具体代码如下:
public class SingleMode3 {
private static volatile SingleMode3 INSTANCE;
//私有化构造方法,不让外界创建对象,只能自己创建
private SingleMode3(){};
//设置外界访问对象的方式,通过getInstance()方法
public static SingleMode3 getInstance(){
//如果synchronized加在方法上,可能会影响性能,因为方法可能有其他业务代码,所以将synchronized加在代码块上
......//业务代码省略
//双重校验,可以保证线程安全
if(INSTANCE==null){ //第一次校验
synchronized (SingleMode3.class){
if(INSTANCE==null){ //第二次校验
//让线程睡1秒,模拟线程之间的抢占
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new SingleMode3();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(SingleMode3.getInstance().hashCode());
}).start();
}
}
}
此时不加volatile ,输出的hashcode也是一样的,那么问题来了:要不要加volatile呢?不加volatile问题会出现在哪呢?
答案是:要加volatile,如果不加volatile,会出现指令重排序的问题。以第一个线程的 "INSTANCE = new SingleMode3();"来说明,如下图所示:
如果有指令重排序的话,可能会发生:还没有对SingleMode3进行初始化时(就是第二步,将初始值赋值给成员变量的过程),就将SingleMode对象的成员变量赋值给INSTANCE(INSTANCE指向变量的内存地址,内存地址是不会变的),也就是第二步和第三步换了个位置,意味着,还没有将真正的初始值赋值给成员变量时,INSTANCE已经有值了(就是第一步的默认值)。如果不加volatile,在超高并发的情况下,如:阿里、京东的秒杀业务,这种情况是可能会发生这样的问题的。加了volatile,对new SingleMode3的指令重排序就不存在了,会在初始化完成之后,才赋值给INSTANCE变量。
以如下代码来说明:
/**
* 运行以下代码,分析结果
*/
public class VolatileTest {
volatile int count = 0;
public void m(){
for(int i=0;i<=10000;i++)count++;
}
public static void main(String[] args) {
VolatileTest v = new VolatileTest();
List<Thread> threads = new ArrayList<>();
for(int i=0;i<=10;i++){
threads.add(new Thread(v::m,"thread-"+i));
}
//启动所有线程
threads.forEach((o)->o.start());
//join方法保证所有线程都执行完
threads.forEach((o)->{
try {
o.join();
}catch (InterruptedException e){
e.printStackTrace();
}
});
System.out.println(v.count);
}
}
分析:即使加了volatile,输出的数量也不会是100000,原因是:虽然加了volatile保证了多个线程间的count值可见性,但是count++本身不是原子性操作,因为count++在Java内部分成好多条指令来执行,所以volatile不能保证多个线程共同修改变量时所带来的不一致问题,也就是说volatile不能代替synchronized。要解决此问题,加synchronized。
JUC包下凡是以Atomic开头的类底层都是使用了CAS锁,不是synchronized重量级锁,而是一种自旋锁,以AtomicInteger举例:
public class AtomicTest1 {
AtomicInteger count = new AtomicInteger(0);
public void m(){
for(int i=0;i<10000;i++){
count.incrementAndGet();//相当于count++,incrementAndGet内部用了cas无锁操作
}
}
public static void main(String[] args) {
AtomicTest1 a = new AtomicTest1();
List<Thread> threads = new ArrayList<>();
for(int i=0;i<10;i++){
threads.add(new Thread(a::m,"thread_"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(a.count); //输出结果为100000
}
}
CAS(compare and set/swap)操作,可以将它理解为一个方法,该方法有三个参数,分别是:要改的值、当前期望的值、要设定的新值,以"将0变成1"的操作为例,要改的值就是0,当前期望的值是0,要设定的值是1。如果当前期望的值不是0,说明有其他线程已经改了,此时会再读一次,然后当前期望的值就是2。CAS在java中是用native方法来实现的,利用了系统本身提供的原子性操作。
这里有一个疑问:当判断这个值是不是期望当前值的过程中,别的线程可以将值改动吗?
答:CAS是CPU的原语支持的,意思就是CAS的操作是CPU级别的操作,中间不能被打断。
ABA问题:假如现在某个线程拿到一个值1,然后要用CAS操作变成2,此时正好别的线程将1变成2,然后又将2变成1,这就是CAS中的ABA问题。而且,如果是基本数据类型,没有ABA问题,如果是引用类型的话,就有ABA问题 ,如图所示:
此时线程1指向的是线程2修改过的对象2,对象2已经发生了一些逻辑改变,就有可能有业务逻辑的问题了。
ABA解决方式:加version(版本号),做任何操作的时候版本号加1,后面检查的时候连版本号一起检查。关于ABA问题以及其解决方案可以参照这篇博文,里面的案例比较生动:https://wenku.baidu.com/view/e3b814f675a20029bd64783e0912a21614797f0c.html
CAS操作是不需要加锁的,那它是怎么做到的呢?其实内部的通过Unsafe类实现的。Unsafe不能直接使用(JDK1.9版本之后),跟ClassLoader有关,所有以Atomic开头的类,内部都是CompareAndSet/CompareAndSwap操作,此操作就是Unsafe类来支持的,Unsafe类直接操作Java虚拟机中的内存。
ReentrantLock可以替代synchronized,但是必须通过try{…}finally{lock.unlock()}手动释放锁,而synchronized是当遇到异常时,自动释放锁。代码如下:
public class ReentrantLockTest {
Lock lock = new ReentrantLock();
void m1(){
try {
lock.lock();
for(int i=0;i<10;i++){
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
if(i==3)m2(); //在m1中调用了m2,m1和m2锁的是同一个对象(就是this对象)
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
void m2(){
try {
lock.lock();
System.out.println("m2() start ");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTest rt = new ReentrantLockTest();
new Thread(rt::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(rt::m2).start();//虽然m2是在m1睡了1秒之后执行的,但是要等到m1执行完才能拿到锁
}
}
ReentrantLock功能较Synchronized的优势:
1)使用reentrantlock可以通过tryLock方法来进行 “尝试锁定” ,无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待。而synchronized如果一直锁不到的话就会wait了,而ReentrantLock可以指定超过时间后要不要继续wait。
public class ReentrantLockTest2 {
Lock lock = new ReentrantLock();
void m1(){
try {
lock.lock();
//循环10次,每次睡1秒钟
for(int i=0;i<10;i++){
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
boolean locked = false;
void m2(){
try {
//此程序中,tryLock尝试5秒钟之内申请锁,如果得不到就抛出异常
locked = lock.tryLock(5,TimeUnit.SECONDS);
System.out.println("m2() "+locked); //此时t2线程是拿不到锁的,因为m1得执行10秒钟,要拿到锁,可以修改m1循环为3,总之小于5就行
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTest2 rt = new ReentrantLockTest2();
new Thread(rt::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(rt::m2).start();
}
}
2)lockInterruptibly,可以对interrupt方法做出响应,就是一旦被打断可以做出响应。而synchronized一旦wait,必须调用相应的方法(如:notify)来唤醒。
3)ReentrantLock公平锁。将ReentrantLock构造方法中参数设置为 "true",就表示是公平锁,意思就是等待队列中等在前面的线程先执行。而synchronized只有非公平锁。演示代码如下:
输出效果为:线程1、2交替获得锁,如果是非公平锁的话,是某个线程执行完,其余线程才能执行,不会出现交替获得锁的情况。
误区:公平锁不是保证线程1执行完释放锁之后线程2立马执行,如果要保证此操作的话,必须得用线程间的通信才可以。没有出现线程1、
线程2顺序执行的原因是:当线程1获得锁执行完打印输出后,释放锁,还没等到线程2进入等待队列,线程1自己进入了队列,然
后又开始执行。
公平锁的关键在于:线程1正在执行的过程中,这时线程2进来,它会先" 看 "队列中有没有等待的线程,如果有的话,它会先进队列中等着,等别的线程先运行,而不是一进来就抢锁,这是公平锁的关键。
public class ReentrantLockTest3 extends Thread{
private static ReentrantLock lock = new ReentrantLock(true);//设置为true表示公平锁
public void run() {
for(int i=0;i<10;i++){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockTest3 rl = new ReentrantLockTest3();
Thread th1 = new Thread(rl);
Thread th2 = new Thread(rl);
th1.start();
th2.start();
}
}
先上代码:
public class ReadWriteLockTest {
static Lock lock = new ReentrantLock();
private static int value;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock){
try {
lock.lock();
Thread.sleep(1000);
System.out.println("read end");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void write(Lock lock,int v){
try {
lock.lock();
Thread.sleep(1000);
value = v;
System.out.println("write end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
// Runnable readR = ()->read(lock); //排他锁(互斥锁)效率比较低
Runnable readR = ()->read(readLock);//共享锁,在这个对象上上了一把读锁的时候,其他的读线程也是可以读的
// Runnable writeR = ()->write(lock,new Random().nextInt());
Runnable writeR = ()->write(writeLock,new Random().nextInt());//写锁是排他的
for(int i=0;i<18;i++) new Thread(readR).start();
for(int i=0;i<2;i++) new Thread(writeR).start();
}
}
说明:ReadWriteLock接口中,定义了读锁和写锁的抽象方法,该方法在其实现类ReentrantReadWriteLock中进行了定义。
Semaphore是信号灯的意思,就是" 看到信号灯了 “就能执行,” 看不到 "就执行不了 。可以用作限流,限定同时执行的线程有多少个,如:买票场景,如果要五个窗口同时买票,通过设置:new Semaphore(5)实现。演示代码如下:
public class SemaphoreTest {
public static void main(String[] args) {
//Semaphore s = new Semaphore(1);//此时Semaphore设置为1,意味着同时运行的线程只能有一个
Semaphore s = new Semaphore(2,true);//默认是非公平的,设置true为公平的
new Thread(()->{
try {
s.acquire(); //获得锁。此线程要想往下执行,必须从Semaphore中获得许可
System.out.println("T1 running");
Thread.sleep(200);
System.out.println("T1 running");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
s.release(); //释放锁
}
}).start();
new Thread(()->{
try {
s.acquire(); //获得锁
System.out.println("T2 running");
Thread.sleep(200);
System.out.println("T2 running");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
s.release(); //释放锁
}
}).start();
}
}
注意:Semaphore和创建线程池是不一样的!创建线程池如果创建数量为2,那线程就有两个,至于线程同步,那就是另一回事了;而new Semaphore(2)的意思是:可以有100个线程,但是同时运行的线程只能有两个(就是同时acquire到的线程有两个),Semaphore作用就是线程同步。
Exchanger可以理解为一个容器,下面的示例代码中,容器中有两个线程,"exchanger()"方法是阻塞方法,当其中一个线程执行此方法时会阻塞,此时等待另一个线程来执行"exchanger()"来交换数据。
public class ExchangerTest {
static Exchanger<String> exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(()->{
String s = "T1";
try {
s = exchanger.exchange(s);//该方法执行到这就阻塞了,等待另一个线程来交换数据
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+s);
},"t1").start();
new Thread(()->{
String s = "T2";
try {
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+s);
},"t2").start();
}
}
思考:String类型的变量s是局部变量,它是怎么进行交换的呢?
分析:Exhanger容器是将"T1" “T2"两个变量的引用放到了容器中,当两个线程都进入容器并交换数据之后,t1线程指向了” T2 “变量,t2线程指向了” T1 "变量。
CountDown是倒数的意思,该操作是原子性的,Latch表示:门闩。CountDownLatch用来等待线程运行结束之后,协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。CountDownLatch能够使一个线程在等待别的线程完成它们各自"业务逻辑"之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了它们各自的任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。主要配合:await、countDown来使用,await用来" 拴住 "线程,当countDown到0之后,await的线程继续往前走。当然等待线程结束也可以用 join。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。代码如下:
public class CountDownLatchTest {
public static void main(String[] args) {
useJoin();
useCountDownLatch();
}
private static void useCountDownLatch() {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);//门栓上面记了个数,是100
for(int i=0;i<threads.length;i++){
threads[i] = new Thread(()->{
int result = 0; //由于Lambda表达式的闭包问题,这里int result = 0;不能提到外面
for(int j=0;j<100000;j++) result += j;
latch.countDown();//此操作是原子性的,每一个线程结束的时候,门闩上的数就会减一
});
}
for(int i=0;i<threads.length;i++)threads[i].start();
try {
latch.await();//执行到这的时候,门栓会" 拴住 ",当门栓上的数变成0时,才会放开
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end latch");
}
private static void useJoin() {
Thread[] threads = new Thread[100];
for(int i=0;i<threads.length;i++){
threads[i] = new Thread(()->{
int result = 0;
for(int j=0;j<100000;j++) result += j;
});
}
for(int i=0; i<threads.length; i++) threads[i].start();
for(int j=0; j<threads.length; j++){
try {
threads[j].join();//每一个线程都等着合并到当前线程上,然后等待线程执行结束
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("end join");
}
}
CountDownLatch的不足
CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
Cyclic循环,Barrier栅栏,循环够一定数量就执行下一步。应用场景:某个线程需要等到其他线程执行完之后才能继续向前执行。
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(20,()->{
System.out.println("满了,发车!");
});
for(int i=0; i<100; i++){
new Thread(()->{
try {
barrier.await();//让当前线程阻塞,够了20个(第一个参数)再执行
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
以上就是我刚开始学习多线程与高并发的一些基本概念和工具,大家觉得有什么疑问,欢迎留言讨论,一起进步,谢谢。