JavaSE高级开发之多线程(中)

线程的同步与死锁

由于每一个线程轮番抢占资源而引起的问题,由于多个线程可以同时进入共享资源,但是由于共享资源有限,线程虽然能同时进入共享资源,但是由于出来的时间不一致,有可能导致资源已经消耗殆尽而还有线程在享用.

解决方案:为这个共享资源"上一把锁",保证同一时间只有一个线程进入共享资源.synchronized称为内建锁。

在Java中,任意一个对象都有一个同步锁:
对象O的同步锁在任何时刻最多只能被一个线程拥有。
如果对象O的同步锁被线程拥有,那么当其他线程来访问O时,这些线程将被放到O的锁池中,并将他们转为同步阻塞态。
拥有O的锁的线程执行完后,会自动释放O的锁,若在执行中,线程T发生异常退出,也将自动释放T的锁。
如果线程在执行同步代码块时,调用了对象的wait()方法,则线程T同样会释放O的锁,线程T也将转为阻塞状态。
如果线程在执行同步代码块时,调用了Thread类的sleep()方法时,线程T将放弃运行权,即放弃CPU,但是对象T不会放弃对象的锁
如果线程放弃了对象的锁,并放弃运行权,则CPU将会随机分配给对象O锁池中的线程,该线程也将拥有对象O的锁。

1.同步代码块

直接将共享资源放入synchronized(this){ }代码块中,表示同一时刻只有一个线能够进入同步代码块,但是多个线程可以同时进入run方法,只有一个线程能够进入synchronized代码块内

public class Block {
    public static void main(String[] args) {
        Runnable runnable=new MyThread();
        Thread thread=new Thread(runnable,"黄牛1");
        Thread thread1=new Thread(runnable,"黄牛2");
        Thread thread2=new Thread(runnable,"黄牛3");
        thread.start();
        thread1.start();
        thread2.start();
    }
}
class MyThread implements Runnable{
private int tick=100;
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            //同步代码块
            synchronized (this){
                if(tick>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"还剩"+this.tick--+"张票");
                }
            }
        }
    }

}

2.同步方法

将共享资源放入一个加锁的方法中(在方法声明中加synchronized),在run方法中调用此共享方法,表示此时只有一个线程能够进入同步方法.

子类可以重写父类的同步方法。若在子类的方法中,使用synchronized关键字修饰,则重写后的方法还是同步方法,否则不是同步方法。

public class Method {
    public static void main(String[] args) {
        Runnable runnable=new MyThread();
        Thread thread=new Thread(runnable,"黄牛1");
        Thread thread1=new Thread(runnable,"黄牛2");
        Thread thread2=new Thread(runnable,"黄牛3");
        thread.start();
        thread1.start();
        thread2.start();
    }
}
class MyThread1 implements Runnable{
private int tick=100;
    @Override
    public void run() {
        for(int i=0;i<100;i++){
           this.sale(tick);
        }
    }
    public synchronized void sale(int tick){

            if(tick>0){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"还剩"+this.tick--+"张票");
            }
        }

}

synchronized上锁会隐式解锁

synchronized对象锁概念

1.synchronized(this)以及普通synchronized方法,只能防止多个线程同时执行同一个对象的同步段,synchronized锁的是括号中的对象,而非代码

public class ObjSynchronized {
    public static void main(String[] args) {
        Syn syn=new Syn();
        for(int i=0;i<3;i++){
        Runnable runnable=new MyRunnable(syn);
            Thread thread=new Thread(runnable);
            thread.start();
        }
    }
}
class  MyRunnable implements  Runnable{
private Syn syn;
public MyRunnable(Syn syn){
    this.syn=syn;
}
    @Override
    public void run() {
       this.syn.test();
    }
}
class Syn{
    public synchronized void test(){
        System.out.println(Thread.currentThread().getName()+ "方法开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "方法结束");
    }
}

2.全局锁:锁代码段.
1使用类的静态同步方法
synchronized与static一起使用,此时锁的是当前使用的类而非对象

class Syn1{
    public static synchronized void test(){
        System.out.println(Thread.currentThread().getName()+ "方法开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "方法结束");
    }
}

2.在代码块中锁当前class对象
synchronized(类名称.class){}

class Syn2{
    public  synchronized void test(){
        synchronized (Syn.class){
            System.out.println(Thread.currentThread().getName()+ "方法开始");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "方法结束");
        }
    }
}

线程获得对象锁时可以获取对象的所有同步方法,因此对象的所有同步方法都会上锁,不能再有其他线程进入

public class SynAply implements Runnable{
       private Sync sync;
      public  SynAply(Sync sync){
          this.sync=sync;
      }
    @Override
    public void run() {
          //因为synchronized是对象锁,因此只能有一个对象进入当前方法
        sync.testA();
        sync.testB();
    }

    public static void main(String[] args) {
        Sync sync=new Sync();
        Runnable synAply=new SynAply(sync);
        Thread threadA=new Thread(synAply,"A");
        Thread threadB=new Thread(synAply,"B");
        threadA.start();
        threadB.start();

    }
}
class Sync{
    public synchronized void  testA(){
        //只有名为A的线程才能执行此方法
        if(Thread.currentThread().getName().equals("A")){
            while(true){

            }
        }
    }
    public synchronized void testB(){
        //只有名为B的线程才能执行此方法
        if(Thread.currentThread().getName().equals("B")){
            System.out.println("线程B打印此方法");
        }
    }
}

synchronized底层实现:

synchronized锁的都是对象,加锁的开销大,线程的阻塞以及唤醒都需要从用户态切换到内核态,因此效率很低
对象锁(monitor机制)是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁

同步代码块的底层实现:

执行同步代码块后首先要执行moniterentor指令,退出时执行monitorexit指令。
使用synchronized实现同步,关键点是要获取对象的监视器monitor对象,当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。
同一时刻只有一个线程可以获取到该对象的monitor监视器。
通常一个monitorenter指令会同时包含多个monitorexit指令。因为JVM要确保所获取的锁无论在正常执行路径或是异常执行路径都能正确解锁。

同步方法底层实现

当使用synchronized标记方法时,字节码会出现访问标记ACC_SYNCHRONIZED,该标记表示在进入该方法时,JVM需要进行monitorenter操作。在退出该方法时,无论是否正常返回,JVM均需要进行monitorexit操作。

当JVM执行monitorenter时,如果目标对象monitor的计数器为 0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器+1;

在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,,JVM可以将计数器再次+1(可重入锁:当线程拥有当前对象的锁后,可以随便进入该对象的所有同步方法,不用每次都加锁);否则需要等待,知道持有线程释放该锁

当执行monitorexit时,JVM需将锁对象计数器减一,当计数器减为零时,代表该锁已经被释放掉,唤醒所有正在等待的线程。

锁优化

使线程排队的时间变短一些

CAS操作(乐观锁)

1.CAS(Compare And Swap)
乐观锁(CAS操作,无锁操作):假设所有线程访问共享资源时不会出现冲突,由于不会出现冲突就不会阻塞其他线程,因此线程就不会出现阻塞停顿状态(我上厕所一定没人跟我抢).出现冲突时,**无锁操作使用CAS(比较交换)来鉴别线程是否出现冲突,**出现冲突就重试当前操作直到没有冲突为止(当我上厕所有人也需要在此事件段上厕所,但是由于资源已被占用,因此,另一个人会不断进来看(重试)资源是否已被释放)

悲观锁:线程获取锁(JDK1.6之前)是一种悲观锁策略,假设每一次执行临界区代码(访问共享资源),都会产生冲突,所以当前线程获取到锁的同时也会阻塞其他为获取到锁的线程.(举例:每次上厕所一定有人和我抢)
CAS操作过程:
CAS可以理解为CAS(V,O,N)
V:当前内存地址实际存放的值
O:预期值(旧值)
N:更新的新值

当V=O时,期望值与内存实际值相等,表面没有被任何其他线程修改过,即值O就是目前最新的值,因此可以将新值N赋值给V,反之如果V!=O,表明该值已经被其他线程修改过了,因此O值不是当前最新值,返回V,无法修改.

当多个线程使用CAS操作时,只有一个线程能成功,其余线程均失败.失败的线程会重新尝试(自旋)或挂起线程(阻塞)

内建锁在优化前(老版本)最大的问题是:在存在线程竞争的情况下会出现线程的阻塞已经唤醒带来的性能问题,这是一种互斥同步(阻塞同步)

CAS不是将线程挂起,当CAS失败后会进行一定的尝试操作并非耗时的将线程挂起.也叫做(非阻塞同步)

CAS问题:

1.ABA 问题更新的值与预期值经过了几次变化后才变为了期望值。
解决:添加版本号或是在JDK1.5以后使用atomic包中提供了AtomicStampedReference来 解决ABA问题
2.自旋会浪费大量处理器资源。因为线程需要不断的重试,并没有用到资源,跑的都是无用的指令。
解决:采用自适应自旋(适用于重量级锁,是重量级锁的优化),根据以往自旋等待的时间动态调整自旋时间。
3.公平性。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于 自旋状态的线程,则很有可能优先获得这把锁。
内建锁无法实现公平机制,而lock体系可以实现公平锁。

Java对象头

在Java对象的头对象中存放对对象的一个标志,Java对象头里的Mark Word里默认的存放的对象的Hashcode (25bits),分代年龄 (4bits),是否偏向锁 (1bit)和锁标记位 (2bits)
JDK1.6后锁一共有4种状态,级别 从低到高依次是:无锁状态(是否偏向锁及锁标记位:0 01)、偏向锁状态(1 01)、轻量级锁状态(无偏向锁 00)和重量级锁状态(无偏向锁 10 ) 11表示GC标记,表示线程被回收

锁只能升级但不能降级,目的是为了提高获得锁和释放锁的效率
在这里插入图片描述

偏向锁

大多数情况下,锁不仅不存在竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销,引入偏向锁。

偏向锁是四种状态中最乐观的锁:从始至终只有一个线程请求某一把锁。

偏向锁的获取当一个线程访问同步块获取锁时,会在对象头中存贮偏向锁偏向的线程ID,以后线程进入或退出同步块不需要进行CAS操作,只需要判断当前线程是否是偏向锁:若是->标识线程已获取锁。
若不是->判断头对象中偏向锁标识是否为1(是否为偏向锁) ;若为0:需要使用CAS竞争锁,并将偏向锁升级为轻量级锁;若为1:使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销(开销较大) 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁.
当持有锁的线程到了全局安全点(没有实际代码在跑)时,它会首先暂停拥有偏向锁 的线程,然后检查持有偏向锁的线程是否活着,若活着,对象头偏向其他线程或者恢复到无锁或者标记该对象不适合作为偏向锁(膨胀为轻量级锁);若线程已经死了,对象头设置无锁状态(0),最后唤醒暂停的线程。

偏向锁头部Epoch字段值:表示此对象偏向锁的撤销次数,默认40次以上,表示此对象不再适合用于偏向锁,当下次线程再次获取此对象时,直接变为轻量级锁。

如何关闭偏向锁
偏向锁在JDK6之后是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。
如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过 JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状

轻量级锁

多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情况,JVM采用了 轻量级锁,来避免线程的阻塞以及唤醒

加锁:线程执行同步块之前,JVM会在当前线程的线桢中创建用于存储锁的记录,将对象头拷贝入记录中,然后线程用CAS竞争将对象头中的Mark Word替换为指向锁记录。如果成功,获得锁,如果失败,表示其他线程竞争锁,锁膨胀为重量级锁。
解锁:使用CAS将拷贝的记录替换回对象头,如果成功,表示没有竞争发生,如果失败,表示锁存在竞争,锁就会膨胀为重量级锁。

三种锁的特点

偏向锁:只会在第一次请求锁时采用CAS操作将锁对象的标记字段记录下当前线程地址,在此后的运行过程中,持有偏向锁的线程无需加锁操作。针对的是锁仅会被同一线程持有的状况。

轻量级锁:轻量级锁采用CAS操作,将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本标记字段。

重量级锁:重量级锁会阻塞、唤醒、请求加锁的线程。针对的是多个线程同时竞争同一把锁的情况。JVM采用自适应自旋,来避免在面对非常多小的同步代码块时,仍然被阻塞和唤醒的状况。

其他优化

锁粗化
就是将对此加锁解锁的操作合并为一次的加锁解锁。将多个连续的锁扩展为范围更大的锁(StringBuffer对象的append方法每次使用都会加锁解锁)
锁解除
删除不必要的加锁操作。根据代码逃逸技术,如果判断一段代码中,堆上的数据不会逃逸出当前线程,则任务此线程是线程安全的,无需加锁。

死锁

一个线程等待另外一个线程执行完毕执行完成后才可以继续执行。但是如果现在相关的几个线程 彼此之间都在等待着,那么就会造成死锁
死锁一旦出现之后,整个程序就将中断执行,所以死锁属于严重性问题。过多的同步会造成死锁,对于资源的上锁 一定要注意不要成"环"。

public class DeadLock {
    private  static Book book=new Book();
    private static  Pen pen=new Pen();

    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (book) {
                    System.out.println(Thread.currentThread().getName() + "我有书,但是还在用");

                    synchronized (pen) {
                        System.out.println(Thread.currentThread().getName() + "我想做笔记,但是没有笔,给我");

                    }
                }
            }
        },"Thread-A");
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (pen) {
                    System.out.println(Thread.currentThread().getName() + "我有笔,但是在用");

                    synchronized (book) {
                        System.out.println(Thread.currentThread().getName() + "想看书,但是没有");
                    }
                }
            }
        },"Thread-B");
thread.start();
thread2.start();
    }
}

class Pen{
    private String name = "笔";

    public String getName() {
        return name;
    }
}
class Book {
    private String name = "书";

    public String getName() {
        return name;
    }
}

线程池

Java线程池是运用最多的并发框架,线程池的优点如下:
1.降低资源消耗:通过重复利用已创建的线程降低线程的创建和销毁带来的消耗。
2.提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
3.提高线程的可管理性:由线程池统一管理调度进行线程分配和监控。

线程池的处理流程
1.首先判断核心线程池里的线程是否都在执行任务。若不是,则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,转到下一个流程。
2.线程池判断工作队列是否已满,如果队列没有满,则交给工作队列处理,若已满则进入下一个流程。
3.线程池判断线程池的线程是否都已处于工作状态,若没有,则创建下一个工作线程执行任务,如果已满,则交给饱和策略处理任务。

线程池的使用

线程池构造方法:

public ThreadPoolExecutor(int corePoolSize,//线程池的基本大小
                              int maximumPoolSize,//线程池最大数量
                              long keepAliveTime,//线程活动保持时间
                              TimeUnit unit,//线程活动保持时间单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              RejectedExecutionHandler handler//饱和策略)

普通方法

void execute(Runnable command) 
在将来某个时候执行给定的任务。 

<T> Future<T> submit(Callable<T> task) 
提交值返回任务以执行,并返回代表任务待处理结果的Future。  

Future<?> submit(Runnable task) 
提交一个可运行的任务执行,并返回一个表示该任务的未来。  

<T> Future<T> submit(Runnable task, T result) 
提交一个可运行的任务执行,并返回一个表示该任务的未来。  

1.执行excute方法

public class TestPool {
    public static void main(String[] args) {
        Runnable myrun=new MyRunn();//创建线程实现类
                                                      //核心线程数    最大线程数           线程活动保持时间
        ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1000,
                //线程活动保持时间的单位   饱和策略
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
        for(int i=0;i<5;i++){
            //用excute()方法提交不需要返回值的任务,不能判断任务是否被线程池执行成功
            pool.execute(myrun);//线程池对象调用excute()方法启动线程,不需调用start方法

        }
    }
}
class MyRunn implements  Runnable{

    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName() + "+" + i);
        }
    }
}

2.sublmit方法

public class TestPoolSubmit{
    public static void main(String[] args) {
        Callable callable=new MyCall();//创建线程实现类

                                                      //核心线程数    最大线程数           线程活动保持时间
        ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1000,
                //线程活动保持时间的单位   饱和策略
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
        for(int i=0;i<5;i++){
            Future<String> future = pool.submit(callable);//利用Future对象接收
            try {
                //通过get(time)方法获取返回值,若指定时间为获取到则返回
                //get()方法会等到任务执行完为止。
                String str= future.get(1000,TimeUnit.MILLISECONDS);
            System.out.println(str);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyCall implements Callable {


    @Override
    public Object call() throws Exception {
        for(int i=0;i<50;i++){
            System.out.println(Thread.currentThread().getName()+"、"+i);
        }
        return Thread.currentThread().getName()+"任务执行完毕";
    }
}

关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的 区别:

shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执 行任务的列表。

shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

合理配置线程池

首先需从以下方面分析任务:
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。
任务的执行时间:长、中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。

CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任 务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太 大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
注:可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先执行。

猜你喜欢

转载自blog.csdn.net/weixin_42962924/article/details/86473954