Java多线程与并发(二)

  • Synchronized线程同步机制
    很多线程同时对同一个数据或者文件进行访问的时候,对于这个文件如果进行并发读写可能会产生问题。
    多线程机制来保证同一个时间,只有一个线程对这个资源进行读写,来保证多线程环境下是健壮的。

代码案例:

public class SynSample {
    public static void main(String[] args) {
        final Couplet c = new Couplet();
        for(int i=0;i<1000;i++){
            new Thread(){
                public void run(){
                    int r = new Random().nextInt(2);
                    if(r%2 == 0){
                        c.first();
                    }else{
                        c.second();
                    }
                }
            }.start();
        }

    }
}
class Couplet{
    //也可以对方法直接加synchronized标识
    Object lock = new Object();//锁对象
    public  void first(){
        synchronized (lock) {//同步代码块--在同一时间只允许一个线程执行访问这个方法

            System.out.printf("琴");
            System.out.printf("瑟");
            System.out.printf("琵");
            System.out.printf("琶");
            System.out.println();   
        }

    }
    public  void second(){
        synchronized (lock){//因为两个同步块指向了同一把锁,所以在同一个时间内,只允许有一个代码块执行,其它等待
            System.out.printf("魑");
            System.out.printf("魅");
            System.out.printf("魍");
            System.out.printf("魉");
            System.out.println();
        }

    }
}

此时输出不会出现交错

多线程环境下的同步机制:

代码中的同步机制:
synchronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁lock(绣球),在多线程(游客)并发访问的时候,同时只允许一个线程(游客)可以获得这个锁,执行特定的代码(迎娶新娘)。执行完成之后释放锁,继续由其他线程争抢!

Synchronized的使用场景:

Synchronize有以下三种场景:对于不同锁对象:

Synchronized代码块–任意对象即可作为锁对象
Synchronized方法–this当前对象作为锁对象—–最常使用
Synchronized静态方法–该类的字节码对象作为锁对象

互斥代码块 必须指向相同的锁对象

public class SynSample {
    public static void main(String[] args) {
        final Couplet c = new Couplet();
        for(int i=0;i<1000;i++){
            new Thread(){
                public void run(){
                    int r = new Random().nextInt(2);
                    if(r%2 == 0){
                        c.first();
                    }else{
                        c.second();
                    }
                }
            }.start();
        }

    }
}
class Couplet{
    //也可以对方法直接加synchronized标识
    Object lock = new Object();//锁对象
    public synchronized void first(){
        /*synchronized (lock) {//同步代码块
*/          System.out.printf("琴");
            System.out.printf("瑟");
            System.out.printf("琵");
            System.out.printf("琶");
            System.out.println();   
        /*}*/

    }
    public  void second(){
        synchronized (this){
            System.out.printf("魑");
            System.out.printf("魅");
            System.out.printf("魍");
            System.out.printf("魉");
            System.out.println();
        }

    }
}
public class SynSample {
    public static void main(String[] args) {
        final Couplet c = new Couplet();
        for(int i=0;i<1000;i++){
            new Thread(){
                public void run(){
                    int r = new Random().nextInt(2);
                    if(r%2 == 0){
                        c.first();
                    }else{
                        c.second();
                    }
                }
            }.start();
        }

    }
}
class Couplet{
    //也可以对方法直接加synchronized标识
    Object lock = new Object();//锁对象
    public synchronized static void first(){
        /*synchronized (lock) {//同步代码块
*/          System.out.printf("琴");
            System.out.printf("瑟");
            System.out.printf("琵");
            System.out.printf("琶");
            System.out.println();   
        /*}*/

    }
    public  static void second(){
        synchronized (Couplet.class){
            System.out.printf("魑");
            System.out.printf("魅");
            System.out.printf("魍");
            System.out.printf("魉");
            System.out.println();
        }

    }
}
  • 线程的五种状态

1.新建(new)
2.就绪(ready)
3.运行(running)
4.阻塞(blocked)
5.死亡(dead)

新建状态—.start()—-就绪状态
就绪状态—-获取CPU时间 run() —-运行状态
运行状态—-I/O、sleep、lock、yield——-阻塞状态
阻塞状态—-获取资源—-就绪状态
运行状态—–死亡状态–线程对象会被销毁

这里写图片描述

死锁的产生:

死锁产生的原因:
死锁是多线程情况下最严重的问题,在多线程对公共资源(文件、数据)进行操作时,彼此不释放自己的资源,而去试图操作其它线程的资源,从而形成交叉引用,就会产生死锁

这里写图片描述
死锁案例(在多核CPU下不会重现)

public class DeadLock {
    private static String a = "A文件";
    private static String b = "B文件";
    public static void main(String[] args) {
        new Thread(){
            public void run(){//线程1来说
                while(true){
                    synchronized (a) {//开启文件A的锁,线程独占
                        System.out.println(this.getName()+":文件A写入");
                    }
                    synchronized (b) {//开启文件B的锁,线程独占
                        System.out.println(this.getName()+":文件B写入");
                    }
                    System.out.println(this.getName()+":所有文件保存");
                }

            }
        }.start();

        new Thread(){
            public void run(){//线程1来说
                while(true){
                    synchronized (b) {//开启文件B的锁,线程独占
                        System.out.println(this.getName()+":文件B写入");
                    }
                    synchronized (a) {//开启文件A的锁,线程独占
                        System.out.println(this.getName()+":文件A写入");
                    }
                    System.out.println(this.getName()+":所有文件保存");
                }

            }
        }.start();
    }

}

解决斯多的最根本的建议:
尽量减少公共资源的引用,用完马上释放
用完马上释放公共资源
减少synchronized的使用,采用副本的方式替代

  • 线程安全
    在拥有共享数据的多线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况

这里写图片描述

线程安全和线程不安全的区别?
线程安全:可靠,执行速度慢,使用建议—需要线程共享时使用
线程不安全:速度快,多线程环境下可能与预期结果不符合,适用建议:在线程内部使用,无需线程间共享

请写出线程不安全的类:
Vector是线程安全的,ArrayList、LinkedList是线程不安全的
Properties是线程安全的,HashSet、TreeSet是线程不安全的
StringBuffer是线程安全的,StringBuilder是线程不安全的
HashTable是线程安全的,HashMap是线程不安全的

  • JDK并发–线程池
    在jdk1.5之前对多线程的支持是有限的,java.util.concurrent包提供了大量支持工具
    并发是伴随着多核处理器的产生而产生的,为了充分利用硬件资源,诞生了多线程技术。但是多线程又存在资源竞争的问题,引发了同步或者互斥的问题,jdk1.5推出的java.util.concurrent(并发工具包)来解决这些问题

什么是线程池

new Thread的弊端:
new Thread()新建对象,性能差
线程缺乏统一管理,可能无限制的新建线程,相互竞争时,严重的话会占用过多资源导致死机或者OOM

ThreadPool—线程池
重用存在的线程,减少对象新建,消亡的开销
线程总数可控,提供资源利用率
避免过多的资源竞争,避免阻塞
提供额外功能,定时执行,定期执行,监控等等

线程池的种类:
在java.util.concurrent中,提供了工具类Executors(调度器)对象来创建线程池可创建的线程池有四种

1.CachedThreadPool—-可缓存的线程池
2.FixThreadPool —定长线程池
3.SingleThreadExecutor—–单线程池
4.ScheduledThreadPool—调度线程池

public class ThreadPoolSample1 {
    public static void main(String[] args) {
        //根据调度对象Executors来创建一个可缓存线程池
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //可缓存线程池的特点:无限大,如果线程池中没有可用的线程创建,有空闲线程时则利用起来
        for(int i =0 ;i<1000; i++){
            final int index = i ;
            threadPool.execute(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println(Thread.currentThread().getName()+"----"+index);
                }
            });
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }//给线程足够的运行时间执行完毕,在进行停止
        threadPool.shutdown();//代表关闭线程池,等待所有线程完成
        threadPool.shutdownNow();//代表立即终止线程池的运行,不等待线程,不推荐使用

    }
}
public class ThreadPoolSample2 {
    public static void main(String[] args) {
        //根据调度对象Executors来创建一个定长线程池
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        //定长线程池的特点:固定线程总数,空闲线程用于执行任务,如果线程都在使用,后续任务则处理等待状态,
        //在线程池中的线程释放掉后(执行完毕后)再执行后续的任务
        //如果线程任务处于等待状态,备选的任务采用等待算法:FIFO :先进先出,LIFO:后进先出 默认采用FIFO

        for(int i =0 ;i<1000; i++){
            final int index = i ;
            threadPool.execute(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println(Thread.currentThread().getName()+"----"+index);
                }
            });
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }//给线程足够的运行时间执行完毕,在进行停止
        threadPool.shutdown();//代表关闭线程池,等待所有线程完成
        threadPool.shutdownNow();//代表立即终止线程池的运行,不等待线程,不推荐使用

    }
}
public class ThreadPoolSample3 {

    public static void main(String[] args) {
        //根据调度对象Executors来创建一个单线程线程池
        //单线程线程池用来作为守护线程--按顺序执行的特点
        //ExecutorService用于管理线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for(int i =0 ;i<1000; i++){
            final int index = i ;
            threadPool.execute(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println(Thread.currentThread().getName()+"----"+index);
                }
            });
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }//给线程足够的运行时间执行完毕,在进行停止
        threadPool.shutdown();//代表关闭线程池,等待所有线程完成
        threadPool.shutdownNow();//代表立即终止线程池的运行,不等待线程,不推荐使用

    }


}

可调度的线程池–每过一段时间触发一个任务

public class ThreadPoolSample4 {
    public static void main(String[] args) {
        //可调度线程池
        ScheduledExecutorService schedulThreadPool = Executors.newScheduledThreadPool(5);
/*      schedulThreadPool.schedule((new Runnable() {
            //延迟3秒执行一次run方法
            @Override
            public void run() {
                System.out.println("延迟3秒执行");

            }
        }), 3, TimeUnit.SECONDS);*/
        //TimeUnit.MINUTES 延迟3分钟

        //按照一定的时间间隔执行 类似Timer类,无论从效率和代码优化,schedulThreadPool优于Timer
        //项目实际开发中,schedulThreadPool和Timer都不会用到,因为会有成熟的框架Quartz来实现定时器--调度框架或者Spring自带的调度框架
        //程序的调度框架支持一个Corn表达式-- 在指定日期、时间执行

        schedulThreadPool.scheduleWithFixedDelay((new Runnable() {
            //延迟3秒执行一次run方法
            @Override
            public void run() {
                System.out.println(new Date()+"延迟1秒执行,每3秒执行一次");

            }
        }), 1, 3, TimeUnit.SECONDS);

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        schedulThreadPool.shutdown();
    }
}

线程池的经典应用:
这里写图片描述

针对数据库而言,时间资源的消耗并不在于sql查询和sql执行,而是建立数据库连接的过程。
避免数据连接消耗的时间,所以采用数据库连接池,在服务启动的时候根据设置的池的大小,来建立数据库连接,供用户使用。(用户使用的时候很快,体验非常好)

30个数据库连接与数据保持有效连接,有30个用户进行访问,直接可以使用连接池中的数据。有任务完成,对已用连接进行释放或者回收,供以后的用户使用
数据库连接池底层就是线程池
Tomcat也含有线程池的经典应用:
MQ消息队列都是线程池的经典应用

  • CountDownLatch倒计时锁
    CountDownLatch倒计时锁特别适合“总-分任务”,例如多线程计算后数据的汇总
    CountDownLatch位于java.util.concurrent(JUC)包下,利用它可以实现类似计数器的功能,比如有一个任务A,它要等待其它3个任务执行完毕后才能够运行,此时就可以利用CountDownLatch来实现,

CountDownLatch本质是一个计数器,从大到小,依次递减

执行原理:
这里写图片描述

代码案例:

public class CountDownLatchDemo {
    private static int count = 0;
    public static void main(String[] args) {
        final CountDownLatch cdl = new CountDownLatch(1000);//cdl和操作数保持一致
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        for(int i=1 ;i<=1000 ;i++){
            final int index = i;
            threadPool.execute(new Runnable() {

                @Override
                public  void run() {
                    synchronized(CountDownLatchDemo.class){
                        try{
                            count =count +index;
                        }catch(Exception e){
                            e.printStackTrace();
                        }finally{
                            cdl.countDown();
                        }

                    }               
                }
            });


        }
        try {
            cdl.await();//阻塞当前线程直到cdl等于0的时候再继续往下执行
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //此处无法猜测运行完毕的时间,所以采用CountDownLatch
/*      try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }*/
        System.out.println(count);
        threadPool.shutdown();
    }
}

cdl.countDown();//尽量放置在finally里面

比如10个excel报表的数据处理结束后,第十一个汇总表要汇总前10个表格内容,前10个线程对应前10个分表的内容,等前10个线程完成数据处理后,再开启第11个线程进行数据汇总

  • Semaphore信号量的使用
    信号量Semaphore作用:
    经常用于限制获取某种资源的线程数量,下面举个例子比如操场5个跑道,一个跑道一次只能有一个学生在上面跑步,一旦所有跑道在使用,那么后面的学生就需要等待,直到有一个学生不跑了

限制资源访问

代码案例;

public class SemaphoreSample {
    public static void main(String[] args) {
        ExecutorService threadPool  = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(5);//定义5个信号量,只允许5个线程同时访问资源
        for (int i = 0; i < 20; i++) {
            final int index = i ;
            threadPool.execute(new Runnable() {

                @Override
                public void run() {
                    try {
                        semaphore.acquire();//获取一个信号量,占用一个跑道
                        play();
                        semaphore.release();//执行完成后释放这个信号量---从跑道退出
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }//

                }
            }); 
        }
        threadPool.shutdown();
    }
    public static void play(){
        System.out.println(new Date()+"----"+Thread.currentThread().getName()+"获得服务器资格");
        try {
            Thread.sleep(2000);
            System.out.println(new Date()+"----"+Thread.currentThread().getName()+"退出服务器");
            Thread.sleep(500);//退出服务器的时间
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

实质上是线程排队机制的实现

另外一个场景:信号量可以决定访问资源的线程数,同时可以决定如果现在没有获取信号量,拒绝提供服务

拒绝服务的案例:

public class SemaphoreSample2 {
    public static void main(String[] args) {
        ExecutorService threadPool  = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(5);//定义5个信号量,只允许5个线程同时访问资源
        for (int i = 0; i < 20; i++) {
            final int index = i ;
            threadPool.execute(new Runnable() {

                @Override
                public void run() {
                    try {
                        if(semaphore.tryAcquire()){//尝试获取一次信号量,true表示获取到,否则返回false
                            play();
                            semaphore.release();//执行完成后释放这个信号量---从跑道退出
                        }else{
                            System.out.println(Thread.currentThread().getName()+"服务器已满,请稍后再试");
                        }
                        //semaphore.acquire();//获取一个信号量,占用一个跑道


                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }//

                }
            }); 
        }
        threadPool.shutdown();
    }
    public static void play(){
        System.out.println(new Date()+"----"+Thread.currentThread().getName()+"获得服务器资格");
        try {
            Thread.sleep(2000);
            System.out.println(new Date()+"----"+Thread.currentThread().getName()+"退出服务器");
            Thread.sleep(500);//退出服务器的时间
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

也可以采用:
if(semaphore.tryAcquire(6,TimeUnit.SECONDS))
表示:尝试获取一次信号量,6s内 返回 true表示获取到,否则返回false

  • CyclicBarrier循环屏障
    CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。与CountDownLatch不同的是该barrier在释放等待线程后可以重用,所以称它为循环的屏障
    百米赛跑(所有选手一起开始)

目的::让所有的线程同时来执行
CyclicBarrier用于让线程必须运行

四个线程位于同一起跑线:
这里写图片描述

案例:创建5个线程间(间隔1s),累计达到屏障点,同时执行:

public class CyclicBarrierSample {
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);//必须有5个线程同时准备好才可以开始运行
    public static void main(String[] args) {
        ExecutorService threadPool  = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            final int index = i ;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            threadPool.execute(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    go();
                }
            });
        }
        threadPool.shutdown();

    }
    private static void go(){
        System.out.println(Thread.currentThread().getName()+"已经准备就绪");
        try {
            cyclicBarrier.await();//设置屏障点,累计5个线程都准备好后才运行后面的代码
            System.out.println(Thread.currentThread().getName()+"开始执行");//累计5个线程后才开始执行此处
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

可以进行线程复用,当运行完5个,之后再新建,可以继续使用原来的线程

应用场景:

CyclicBarrier适用于多线程必须同时开始的场景(比如:秒杀)
性能测试软件—–测试负载(所有线程同一时间同时开始执行,测试CPU性能极限)
秒杀活动—–

预制20个线程,定点时间线程同时跑,获取线程对应的中奖人,
抢票机器人–做线程同步

任何多线程同时运行的场景可以使用CyclicBarrier

  • 重入锁(ReentrantLock)
    重入锁实质任意线程在获取到锁之后,再次获取该锁而不会该锁所阻塞

ReentrantLock设计的目标是用来替代synchronized关键字
想法很美好,但是各有优缺点

这里写图片描述

synchronized依赖于JVM,reentrantLock依赖于JDK
synchronized在jdk1.5之后,持续优化,–偏量锁,轻量锁–会让其性能大大提升,但从性能上和reentrantLock区别不是很大
synchronized编码简单,reentrantLock编码复杂,必须手动释放锁,reentrantLock含有高级功能(锁的粒度比较细–读锁和写锁)、公平锁(按照线程等待的时间长短来获取锁)和非公平锁(线程中优先级高的锁可以优先获取之前线程释放的锁),可以进行中断、Condition分组唤醒

不推荐使用reentrantLock,因为很多高级功能在开发中用到的很少,除非设计框架设计等等,使用角度来说,越简单越好

//使用锁机制实现线程安全

public class DownloadSimple2Lock {

    private static int user = 1;//同时模拟的并发用户访问数量
    //private static int user = 10;
    private static int dowloadCounts = 5000;//用户的真实下载数
    private static int count= 0;//计数器

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        //调度器,jdk1.5之后引入current对于并发的支持

        ExecutorService executor = Executors.newCachedThreadPool();
        //信号量 ,用于模拟并发用户数
        final Semaphore semaphore = new Semaphore(user);
        for(int i =0;i<dowloadCounts ;i++){
            //通过多线程模拟多个用户访问的下载次数
            executor.execute(new Runnable() {

                @Override
                public void run() {
                    try{
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    }catch(Exception e){
                        e.printStackTrace();
                    }

                }
            });
        }
        try {
            //延迟主线程结束--让for循环中代码执行完毕
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
    private  static void add(){
        lock.lock();//上锁
        try{
            count++;
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();//释放锁,此处必须在finally中进行释放,否则一旦发生异常,则可能无法释放锁,
            //从而导致死锁的产生
        }


    }
}

此处需要重点掌握synchronized和ReentrantLock实现线程安全的优缺点

猜你喜欢

转载自blog.csdn.net/qq_19704045/article/details/81613010