Java多线程并发相关

线程

创建线程的方式

1、继承Thread

public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Resources();
        thread.start();
    }

}
class Resources extends Thread {
        @Override
        public void run(){
            System.out.println("Hello World");
        }
}

2、实现Runnable

public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new Resources());
        thread.start();
      //jdk1.8以后也可以这样写
//        new Thread(()->{}).start();
    }

}
class Resources implements Runnable {
        @Override
        public void run(){
            System.out.println("Hello World");
        }
}

3、实现Callable

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<String>(new Resources());
        new Thread(futureTask).start();
        //Callable是带有返回值的
        System.out.println(futureTask.get());
    }

}
class Resources implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Hello World";
    }
}

4、线程池方式
  阿里巴巴Java开发规范中曾提到,不建议使用Executors创建线程池,如果用的是idea开发工具的话也会给出提示,并给出正确的模板。在这里插入图片描述

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadTest {
    public static void main(String[] args) {
        CustomThreadFactory customThreadFactory = new CustomThreadFactory();
        ExecutorService pool = new ThreadPoolExecutor(5, 200,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(1024),
                //这儿可以用自定义线程工厂来生成新的线程,如果不写可以采用默认的Executors.defaultThreadFactory()
                customThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        // 自定义线程工厂里下面写死了就生成Resources这个线程 所以这儿传ResourcesTwo()也不起作用
        pool.execute(new Resources());
        pool.execute(new ResourcesTwo());
        pool.shutdown();//gracefully shutdown
        Executors.newCachedThreadPool();
    }

}

class Resources implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
}

class ResourcesTwo implements Runnable {
    @Override
    public void run() {
        System.out.println("Hi~");
    }
}

class CustomThreadFactory implements ThreadFactory {
    /**
     * 自定义线程工厂实现记录线程数 如果使用默认的Executors.defaultThreadFactory()则就只有生成线程的功能
     */

    AtomicInteger count = new AtomicInteger();

    @Override
    public Thread newThread(Runnable r) {
        System.out.println(count.incrementAndGet());
        return new Thread(new Resources());
    }
}

线程的生命周期

线程的生命周期包含5个阶段,包括:创建、就绪、运行、阻塞、结束。
New 一个线程之后处于新建状态,
Start 后处于就绪状态,这时候线程处于等待CPU分配资源阶段,谁拿到CPU资源,谁开始执行,
获得 CPU 可执行后处于运行状态,
在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态
一切正常非正常结束后会变成结束。
在这里插入图片描述

线程上下文切换

  CPU把当前执行的任务暂时放下然后去执行另外的任务,就这样按照设置的调度方式经过多次调度又切换回原来正在执行的任务,这个过程就是上下文切换。上下文切换包括线程切换、进程切换、模式切换、地址空间切换,此处只讲一下线程的切换。
  理论上来讲程序有多个线程往往会比单线程更快,更能够提高并发,但提高并发并不意味着启动更多的线程来执行。更多的线程意味着线程创建销毁开销加大、上下文非常频繁,程序反而不能支持更高的TPS(每秒处理的事务数量)
  单核CPU也可以支持多线程是因为CPU可以通过时间片轮转的方式让多个线程获取到CPU资源去执行任务。因为时间片非常短,所以CPU通过不停地切换线程执行,多核心的CPU可以减少切换的次数从而提高效率。
线程切换的调度方式有抢占式调度和协同式调度,JVM就是用的抢占式调度。抢占式调度就是当有了CPU资源时谁抢到算谁的,协同是调度就是排队执行。程序让出cpu的情况有这么几种:
1、当前运行线程主动放弃CPU,JVM暂时放弃CPU操作 例如调用yield()方法。
2、当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
3、当前运行线程结束,即运行完run()方法里面的任务。
结合这个特点,因此优化手段有:
1、无锁并发编程,多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据,类似于ConcurrentHashMap的分段锁思想。
2、CAS算法,Java的Atomic包使用CAS算法来更新数据,底层是原子操作,不需要加锁。
3、使用最少线程
4、单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
并发很高但是每个线程处理时间很短的情况,建议减少线程数量。
并发低但是每个线程处理时间很长的情况,建议增加线程数量。
并发和耗时都很高的时候,要分析任务类型、增加排队、加大线程数

线程的基本方法

线程的基本方法有 start、stop、interrupt、sleep、join、wait、notify、yield
start 是启动线程
stop 是强制终止线程会一下释放所有的锁不安全
interrupt 改变线程状态为中断并抛出异常,用 catch 捕获异常后用 isinterrupt手动结束线程
sleep 线程睡眠 与 wait 不同的是 sleep 是 Thread 类的方法 wait 和 notify 是Object 类的, sleep 不释放锁,到点自动唤醒 wait 释放锁且需要手动唤醒
join 父线程等待子线程执行完毕再继续执行使用 join
yield 线程让步,重新开始竞争资源,有可能本线程重新抢到资源
interrupt、 interrupted 和 isInterrupted 的区别
interrupt 改变线程状态为中断并抛出异常并不是真正的中断线程,会抛出个异常,需要在 catch 块中用 Thread.currentThread().isInterrupted()来判断线程是否处于中断状态然后再根据判断进行处理。
interrupted 是 Thread 类的静态方法,作用是查看当前线程的中断状态,若为中断状态则清除中断。这个方法在哪写就是检测那个线程的状态。 比如在父线程里写 t1.interrupted(),虽然 t1 是子线程的对象但是返回的确是父线程的状态

线程池

线程池原理

  线程池就是个管理线程的池子,帮助我们管理线程,线程重复利用,减少资源消耗,提高响应速度。
默认情况下,线程池在初始的时候,线程数为 0, 当接收到一个任务时,如果线程池中存活的线程数小于 corePoolSize 核心线程,则新建一个线程,这个线程就是核心线程。核心线程即使执行完任务会被回收而不是结束,为了复用时不需要创建拿来直接用。 如果所有运行的核心线程都在忙,超出核心线程处理的任务,执行器更多地选择把任务放进队列,而不是新建一个线程。 如果一个任务无法提交到队列,在不超出最大线程数量情况下,会新建线程此时的线程就不再是核心线程了,执行完后就结束了。超出了最大线程数就会进入报错处理流程。
  形象化的例子比如我有个袋子里面最多放 10 台手机,核心手机数是 5。我刚有这个袋子时候里面肯定是空的,然后当我要打电话时我会放进去一个手机并开机然后打电话,直到达到袋子里有 5 个手机并保持开机,此时再有打电话的需求那么会判断这 5 台手机是否有空闲,若有空闲就直接打电话就可以了,若没有空闲那就排队等候(阻塞队列)。当这个阻塞队列中排队等着打电话的也满了之后那就看看袋子里还能否再放进去一台新的手机。若可以那么放进去一台手机然后打电话,打完后会根据 keepAliveTime 等一会看看还有没有非核心线程的任务,若有继续执行没有的话到点就关机而不是待机。因为此时的线程不再是核心线程。若 袋 子直 接 装不 下了 那 么 执 行拒 接 策略 。 当 袋子 里 长时 间 没有 任务 时 可选 择修 改allowCoreThreadTimeOut 自动关闭(默认线程池不会自动关闭的),也可以手动关闭

线程池的参数

线程池的初始化参数

 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                              BlockingQueue<Runnable> workQueue ,
                              ThreadFactory threadFactory,RejectedExecutionHandler handler)  
                               
  corePoolSize: 线程池核心线程数
  maximumPoolSize: 线程池最大线程数大小,keepAliveTime: 线程池中非核心线程空闲的存活时间大小,该参数只在
    线程数大于corePoolSize 且阻塞队列满了时候时才有用, 超过这个时间的空闲线程将被终止;
  unit: 线程空闲存活时间单位有 7 种取值(天、小时、分钟、秒、毫秒、微秒、纳秒)
  workQueue: 存放任务的阻塞队列
    ArrayBlockingQueue 有界队列、底层数组、 先进先出
    LinkedBlockingQueue 有界队列、底层链表、先进先出、性能优于 ArrayBlockQueue
    DelayQueue 使用优先级队列实现的无界阻塞队列。可指定过多少时间后取到元素,可用于缓存失效、定时任务、网吧计时等
    PriorityBlockingQueue 支持优先级排序的无界阻塞队列。 可自定义排序逻辑
    SynchronousQueue 不存储元素的阻塞队列 里面同时只有 1 个元素每次 put 后必须get 了才能再 put
  threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题,此参数非必填。
  handler: 线城池的饱和策略事件,主要有四种类型:
	AbortPolicy(丢弃并抛出一个异常,默认的)
	DiscardPolicy(直接丢弃任务)
	DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
	CallerRunsPolicy(交给线程池调用所在的线程进行处理                                                        

锁的几种类型

CAS

  CAS全称是叫比较并交换(Compare And Swap),是一种理论依据,即读取数据并进行修改时,认为在读和改之前不会被篡改,然后在修改数据时比较一下当前拿到的数据和数据库里已有的数据是否一致,如果一致则进行更新,如果不一致则不更新。自旋锁、乐观锁都是CAS的思想实现,AtomicInteger等原子类中的compareAndSet等操作是CAS具体到类上的实现。
  CAS思想的优点是不用锁住资源能提高效率,但是缺点是存在ABA问题,ABA就是某个值一开始等于1,线程A拿到后经过一系列操作要改成3,但在这个时间范围内,线程B把这个值从1改成2,又从2改成了1,A在修改时发现值并没有发生变化,这样虽然看起来不影响这个值但是可能会带来其他的问题,比如ABC三个人同时操作同一账户,开始时账户有100元,C一开始读取到的余额是100,在取钱时候操作慢了,A这时候存了50,B立马取出,C取钱时余额还是100,此时A存的50已经被取走了,C还不知情。
  解决ABA问题的最简单办法就是加个版本号,比如三人最开始拿到的余额和版本号是100和1,A操作了版本号+1,B操作了版本号再加1,等C真正取的时候发现自己的版本号和现有的版本号不一致了这时他取钱失败提示账户资金已经变动了需要重新查看余额再重新取钱,具体到代码实现时,JUC包中提供了一个名为AtomicStampedReference的工具类,可以再执行compareAndSet时不仅对比值还对比版本号。

   public static void main(String[] args) throws InterruptedException {
        AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
        new Thread(() -> {
            Integer stamp = stampedReference.getStamp();//1
            //休眠一秒,让线程t2拿到初始的stamp
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(stampedReference.compareAndSet(100, 101, stamp, stamp + 1));
            Integer stamp1 = stampedReference.getStamp(); //2
            System.out.println(stampedReference.compareAndSet(101, 100, stamp1, stamp1 + 1)); 
            System.out.println("t1线程两次修改后的stamp值: " + stampedReference.getStamp());
        }, "t1").start();

        new Thread(() -> {
            Integer stamp = stampedReference.getStamp();//1
            try {
            	//休眠3秒让出CPU资源 模拟ABA问题
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2线程获取到的初始stamp: " + stamp);//1
            System.out.println("t2使用初始stamp修改结果: " + stampedReference.compareAndSet(100, 2019, stamp, stamp + 1));
        }, "t2").start();
    }
--------------------------输出内容------------------------------
true
true
t1线程两次修改后的stamp值: 3
t2线程获取到的初始stamp: 1
t2使用初始stamp修改结果: false

自旋锁、乐观锁、悲观锁

  自旋锁、乐观锁都属于CAS思想的一种实现,就是读和改之间乐观的任务这个数据不会在这期间被篡改,悲观锁反而是认为在读和写之间这个数据肯定会被被人篡改,要防止篡改。自旋锁就是比较并交换过程中在比较阶段发现不一致然后就在那原地循环等待直到比较结果一致再进行更新。优点是当线程数量较多且每个线程处理时间短时,可以减少线程上下文切换从而提高效率,但是缺点是如果它一直拿不到资源则会一直浪费CPU资源。处理方法是加个时间控制,超过一定时间后退出不再更新。

//自旋锁的简单实现
        AtomicInteger atomicInteger = new AtomicInteger(1);
        new Thread(() -> {

            long startTime = System.currentTimeMillis();
            //自旋锁实现
            while (true){
                //一开始是1 这个地方肯定是更新失败无法break,然后就一直在while true里自旋 此处也体现了乐观锁的思想
                if(atomicInteger.compareAndSet(2, 3)){
                    break;
                }
                //如果超过了5秒还没更新则放弃更新不一致占用资源
                if(System.currentTimeMillis()-startTime>5000){
                    break;
                }
            }
            System.out.println("t1更新为"+atomicInteger.get());
        }).start();

        new Thread(() -> {
            try {
                //模拟T2线程后来把值改为了2,让上面的条件满足进行更新 停止自旋
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2更新为2");
            atomicInteger.compareAndSet(1, 2);
        }).start();
    }

互斥锁、读写锁

  互斥锁就是某个资源同一时刻只能被一个线程单独使用,比如某个文件如果加了互斥锁,无论读还是写那同一个时刻只能有一个线程进行操作,互斥锁的应用有Synchronized、ReentrentLock。而读写锁就是为了提高效率把读写操作分离成两个锁互不影响,读写锁的应用是ReadWriteLock。其中写锁是互斥锁也叫排他锁或独占锁,写锁拿到锁的条件是某一时刻既没有读锁也没有写锁。而读锁是共享锁,线程拿到读锁的条件是某一时刻只要没有加写锁就可以,即使有别的线程拿到读锁也不影响再次上读锁,其作用就是保证每个线程拿到的都是写锁释放后的数据。

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadResource resource = new ThreadResource();
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 1; i++) {
            new Thread(() -> {
                resource.write();
                latch.countDown();
            }, "Thread0").start();

            new Thread(() -> {
                try {
                    resource.read();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();
            }, "Thread1").start();
            new Thread(() -> {
                try {
                    resource.read();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();
            }, "Thread2").start();
        }
        latch.await();
        resource.print();
        System.out.println("-----------------分隔线-----------------");
        //互斥锁
        new Thread(() -> resource.add()).start();
        new Thread(() -> resource.add()).start();

    }
}

class ThreadResource {
    private Integer size = 0;
    private final Object mutex = new Object();

    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    public void add() {
        //互斥锁
        synchronized (mutex) {
            size++;
            System.out.println(size);
            try {
                //模拟独占3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void read() throws InterruptedException {
        long startTime = System.currentTimeMillis();
        while (true) {
            System.out.println(Thread.currentThread().getName() + "在等待>>>");
            if (readLock.tryLock()) {
                try {
                    System.out.println("线程" + Thread.currentThread().getName() + "获取到读锁,读取到数值为:" + size);
                } finally {
                    readLock.unlock();
                }
                break;
            }
            if (System.currentTimeMillis() - startTime > 5000) {
                break;
            }
            Thread.sleep(600);
        }
    }

    public void write() {
        writeLock.lock();
        try {
            size += 1;
            //模拟写一段时间 可以看到读的线程一直在等待写锁释放
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println("释放写锁");
        }

    }

    public void print() {
        System.out.println(size);
    }


}

公平锁、非公平锁

  公平锁就是多个线程按先来后到顺序去获得锁,非公平锁就是不按照顺序获取锁,谁抢到算谁的。Java常见的锁如synchronized、reentrentLock都是非公平锁的实现。
  公平锁优点所有的线程都能得到资源,不会饿死在队列中。缺点是吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
  非公平锁的优点是可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点是这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,超过自旋时间后导致饿死。ReentrantLock、ReentrantReadWriteLock的构造方法中都提供了fair参数用于选择使用公平锁还是非公平锁。

	//true公平锁  false非公平锁 如果不写默认都是使用的非公平锁
	Lock lock = new ReentrantLock(true);
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock(true);
   

当使用公平锁时底层源码AbstractQueuedSynchronizer类中有个hasQueuedPredecessors的方法判断当前的线程是不是位于同步队列的首位,从而实现公平排队执行
在这里插入图片描述

Synchronized

  Synchronized是最常见的可重入锁,它相比于lock省去了手动释放锁的过程。
它具有原子性、可见性和有序性的特点。原子性就是被它修饰的内容必须作为一个整体全部执行完,比如用synchronized修饰了3行代码,那么这三行代码就是一个整体,执行这三行代码时CPU上下文不会切换到其他线程。可见性就是多个线程访问同一个被synchronized修饰的资源时,这个资源的状态、值等信息其他线程都是可见的,并且会再释放锁之前把资源的最新数据刷新到共享内存中,保证其他线程拿到的是最新值。有序性是指程序执行的顺序按照代码先后执行。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
  Synchronized可以修饰实例方法、静态方法、代码块。
1、修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

synchronized void func() {
  //业务代码
}

2、修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

synchronized void staic func() {
  //业务代码
}

修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

//对象锁
synchronized(this) {
  ....
}
//类锁
synchronized(Resources.class) {
  ...
}

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
###$ Synchronized原理
  synchronized 是有JVM来保证其同步的,synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。在 Java 虚拟机(HotSpot)中,每个对象中都内置了一个 ObjectMonitor对象。
  另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
  在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
  在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
使用javap -c -s -v -l ThreadTest.class命令可以看到jvm生成的字节码,从字节码中可以看到,再在输出123之前加了一个monitorenter,执行完之后又执行了monitorexit释放锁在这里插入图片描述
  注意当synchronized修饰代码块和修饰方法时他们的实现方式是不太一样的。synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
在这里插入图片描述
  synchronized用的锁是存在Java对象头里的。Java的每个对象的对象头都有个区域专门放monitor监视器,当这个monitor被持有后它处于锁定状态。通过这个状态的值来标识这个对象是否已经上锁且可以通过不同的值来代表锁的状态。在这里插入图片描述

锁的几种状态

  sychnorized在jdk1.5之后进行了较大的优化,以前就是个重量级的锁,后来引入了锁升级的概念进行了优化。锁主要存在四种状态,从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,而且锁的升级可以跳级。

偏向锁

  偏向锁就是对象头中会记录当前线程的ID,如果该线程再次获取锁时,会对比和对象头(这个区域叫MarkWord)中的线程ID是否一致,如果一致则不再需要加锁和解锁,

轻量级锁

  如果不一致则会利用CAS算法替换线程ID,并把这个线程ID复制到另一个专门存着锁记录的内存区域(这个区域叫Displaced Mark Word)然后给新线程加锁,这个过程就是偏向锁升级为轻量级锁,如果抢不到锁就会一直自旋直到拿到锁(这段时间会耗费CPU资源)或者是等锁升级成重量级锁后再阻塞等待其他线程唤醒(阻塞状态是不耗费CPU资源的)

重量级锁

  当轻量级锁解锁时若发现DisplacedMarkWork 和MrakeWord中的线程ID不一致了,说明当前锁存在竞争,锁就会膨胀成重量级锁,锁的升级过程是不可逆的,锁处于重量级状态时,当线程拿不到锁时会变成阻塞状态等待持有锁的线程释放资源唤醒而不是像轻量级锁那样自旋。

区别

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距如果存在线程竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块的场景
轻量级锁竞争是线程不会阻塞,提高了程序的响应速度如果始终得不到锁则一直自旋占用CPU资源追求响应时间,同步块执行速度非常快
重量级锁线程不适用自旋,不会耗费CPU资源线程阻塞需要等他其他线程唤醒,响应慢追求吞吐量,同步块执行时间长

  偏向锁默认是启用的,如果确定程序里面的所有锁通常处于竞争状态则可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
  知道这个感觉并没啥用,只是个理论基础,实际工作中从没用到过。。。

Voliatile关键字

  Voliatile关键字的作用是:1、保证了被修饰属性的可见性;2、禁止指令重排序。如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令,lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能: 1.重排序时不能把后面的指令重排序到内存屏障之前的位置 2.使得本CPU的工作内存写入主存 3.写入动作也会引起别的CPU或者别的内核无效化其工作内存,相当于让新写入的值对别的线程可见。单例模式的实现就使用了Voliatile是典型的双重检查锁定(DCL)

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

保证可见性

  如果一个程序有2个线程,CPU是双核的,我们知道线程中运行的代码最终都是交给CPU执行的,假如这2个线程被分在了两个CPU核心去执行代码,而代码执行时所需使用到的数据来自于内存(或者称之为主存)且修改后再写会主存。但是CPU是不会直接操作主存的,因为CPU执行指令的速度是很快的,但是主存访问的速度就慢了很多,相差的不是一个数量级,所以CPU里加了好几层高速缓存就是常见的一级缓存、二级缓存有时候也叫L1、L2。操作这些缓存的速度比操作主存更快。
  在线程执行时,首先会从主存中读取变量值,再加载到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。问题就处在工作内存这一步,因为每个CPU操作的是各自的缓存,所以不同的CPU之间是无法感知其他CPU对这个变量的修改的,最终就可能导致结果与我们的预期不符。
  而使用了volatile关键字之后,情况就有所不同,volatile关键字有两层语义:
    1、立即将缓存中数据写会到内存中
    2、其他处理器通过嗅探总线上传播过来了数据监测自己缓存的值是不是过期了,如果过期了,就会对应的缓存中的数据置为无效。而当处理器对这个数据进行修改时,会重新从内存中把数据读取到缓存中进行处理。在这种情况下,不同的CPU之间就可以感知其他CPU对变量的修改,并重新从内存中加载更新后的值,因此可以解决可见性问题
在这里插入图片描述

禁止指令重排序

指令重排序就是JVM运行代码时可以调整代码的顺序,禁止重排序,可以确保程序的有序性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值