• 线程同步


     线程同步

    1.synchronized

    2.wait、notify

    3.线程安全与非安全

      StringBuffer 、StringBuilder

      Vector、Hashtable

      ArrayList、HashMap

      Collections.synchonizedList()

      Collections.synchronizedMap()

    4.ExecutorService

    5.BlockingQueue

    ----------------------------------------------------------------------------

    1. synchronized同步锁
    synchronized 
    1.可以修饰方法 被修饰的方法 称为同步方法
    2.synchronized修饰代码块,用于同步某一块代码码片段的, 通常synchronized块的范围要小于synchronized方法

    多个线程并发读写同一个临界资源时候会发生"线程并发安全问题“
    常见的临界资源:
    多线程共享实例变量
    多线程共享静态公共变量
    若想解决线程安全问题,需要将异步的操作变为同步操作

    何为同步?那么我们来对比看一下什么是同步什么异步。
    所谓异步操作是指多线程并发的操作,相当于各干各的
    所谓同步操作是指有先后顺序的操作,相当于你干完我再干

    而java中有一个关键字名为:synchronized,该关键字是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。

     1 /**
     2  * 线程安全问题
     3  *多线程并发访问同一段数据的时候  就会产生线程安全问题
     4  *解决办法:  把异步操作变成同步操作(先后顺序)
     5  *synchronized  同步锁   也有人叫 互斥锁
     6  */
     7 class SynDemo{
     8     public static void main(String[] args) {
     9         final Table table =new Table();
    10         Thread t1 =new Thread(){
    11             @Override
    12             public void run() {
    13                 while(true){
    14                     System.out.println(getName()+":"+table.getBean());
    15                     Thread.yield();
    16                 }
    17             }
    18         };
    19         
    20         Thread t2=new Thread(){
    21             @Override
    22             public void run() {
    23                 while(true){
    24                     System.out.println(getName()+":"+table.getBean());
    25                     Thread.yield();
    26                 }
    27             }
    28         };
    29         t1.start();
    30         t2.start();
    31         
    32     }
    33 }
    34 class Table{
    35     //桌子上有20个桌子
    36     private int beans =20;
    37     //从桌子上取出一个豆子
    38     public synchronized int getBean(){
    39         if(beans==0){
    40             throw new RuntimeException("没有豆子了");
    41         }
    42         Thread.yield();//主动让cpu回到Runnable
    43         return beans--;
    44     }
    45 }
    46 /**
    47  * 线程安全的互斥问题
    48  * @author Administrator
    49  *当一个类中多个方法被synchronize修饰时 这些方法一般是互斥的
    50  */

    2. 锁机制


    Java提供了一种内置的锁机制来支持原子性:
    同步代码块(synchronized 关键字 ),使用同步块的目的: 在于缩小同步范围来提高并发效率

    同步代码块包含两部分:
    a、作为锁的对象的引用,
    b、作为由这个锁保护的代码块。

    synchronized (同步监视器—锁对象引用this){ 
      //代码块
    } 

    通常 同步监视器-锁对象引用 写的是this
    若方法所有代码都需要同步也可以给方法直接加锁
    每个Java对象都可以用做一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时怎释放锁,而且无论是通过正常路径退出锁还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

    3. 选择合适的锁对象

    使用synchroinzed需要对一个锁对象上锁以保证线程同步。
    那么这个锁对象应当注意:多个需要同步的线程在访问该同步块时,看到的应该是同一个所对象引用。否则达不到同步效果。通常我们会使用this来作为锁对象。
    同步块要想有同步效果,多线程看到的同步锁对象,必须是同一个

    4. 选择合适的锁范围

    在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。


    5. 静态方法锁

    当我们对一个静态方法加锁,如:

    public synchronized static void xxx(){
    ….
    }

    那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
    静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。
    原因在于,静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
    静态方法上锁以后 同步是跨对象的

    **
     * 静态方法锁
     * 静态方法上锁后,同步是跨对象的
     * @author Administrator
     *
     */
     public class StaticDemo {
         public static void main(String[] args) {
            
        }
        public void methodA(){
            String name =Thread.currentThread().getName();
            System.out.println(name+"调用了methodA方法");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println("调用MethodA方法完毕");
            }
        }
        public synchronized static void methodB(){
            String name =Thread.currentThread().getName();
            System.out.println(name+"调用了methodB静态方法");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println("调用MethodB方法完毕");
            }
        }
    }
    
     
     class TestDemo{
         public static void main(String[] args) {
            final StaticDemo sd1 =new StaticDemo();
            final StaticDemo sd2 =new StaticDemo();
            Thread t1 =new Thread(){
                @Override
                public void run() {
                    sd1.methodB();
                }
            };
            Thread t2 =new Thread(){
                @Override
                public void run() {
                    sd2.methodB();
                }
            };
            
            t1.start();
            t2.start();
            
        }
     }
    /**
      * 编写计时线程,每隔5秒钟输出当前的日期-时间,
      * 主线程结束后计时完毕
      * @author Administrator
      *
      */
     class Homework1{
         /*
          * 1.创建一个线程 用于计时
          * 2.线程计时 
          *      2.1 创建SimpleDateFormate
          *      2.2循环一下操作
          *      2.3创建Date实例 表示系统时间
          *      2.4使用SimpleDateFormate将Date转换为字符串输出
          *      2.5阻塞线程5000毫秒
          * 3.设置线程为守护线程
          * 4.线程启动
          * 5.为了保证守护线程可以运行一段时间 我们阻塞main线程10秒钟
          */
         public static void main(String[] args) {
             
            
             Thread t1 =new Thread(){
                 @Override
                public void run() {
                     while(true){
                             SimpleDateFormat sdf =new SimpleDateFormat("yy-MM-dd HH:mm:ss");
                            Date now =new Date();
                            System.out.println(sdf.format(now));
                            try {
                                Thread.sleep(3000);
                            } catch (InterruptedException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                }
             };
             t1.setDaemon(true);
             t1.start();
             try {
                //如果不阻塞main 只剩下守护进程的时候 gc直接调出 结束进程了
                 Thread.sleep(10000000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
             
        }
     }

    6、wait和notify

    多线程之间需要协调工作。
    例如,浏览器的一个显示图片的 displayThread想要执行显示图片的任务,必须等待下载线程downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。
    以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒
    在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。

    wait    (阻塞)     可以在当前对象身上等待
    notify   (解除阻塞)    调用哪个对象的notify方法 就可以让在该对象身上等待的线程继续运行

    join比较被动 需要都运行完 才会 解除阻塞
    wait方法要求: 调用哪个对象的wait的方法 就要将该对象加锁

     1 /**
     2   * wait 和notify方法
     3   * 这两个方法是定义在Object上的
     4   */
     5  class WaitAndNotify{
     6      private static boolean isFinish =false;
     7      public static void main(String[] args) {
     8         //用一个对象测试wait和notify
     9          final Object obj =new Object();
    10          //下载线程
    11          final Thread download =new Thread(){
    12              @Override
    13             public void run() {
    14                 System.out.println("图片开始下载:");
    15                 for(int i=0;i<50;i++){
    16                     System.out.println("图片下载%"+i);
    17                     try {
    18                         Thread.sleep(10);
    19                     } catch (InterruptedException e) {
    20                         // TODO Auto-generated catch block
    21                         e.printStackTrace();
    22                     }
    23                 }
    24                 System.out.println("图片下载完毕");
    25                 isFinish=true;
    26                 
    27                 //通知显示线程可以开始工作了
    28                 synchronized (obj) {
    29                     //obj.notifyAll();  随机选择线程 解除阻塞
    30                     obj.notify();
    31                 }
    32                 System.out.println("附件开始下载:");
    33                 for(int i=0;i<50;i++){
    34                     System.out.println("附件下载%"+i);
    35                     try {
    36                         Thread.sleep(10);
    37                     } catch (InterruptedException e) {
    38                         // TODO Auto-generated catch block
    39                         e.printStackTrace();
    40                     }
    41                 }
    42                 System.out.println("附件下载完毕");
    43             }
    44          };
    45          
    46          Thread show =new Thread(){
    47              @Override
    48             public void run() {
    49                  System.out.println("开始显示图片");
    50 //                 try {
    51 //                    download.join();
    52 //                } catch (InterruptedException e) {
    53 //                    // TODO Auto-generated catch block
    54 //                    e.printStackTrace();
    55 //                }
    56                  //在obj对象上等待
    57                  try {
    58                      synchronized (obj) {
    59                          /*
    60                           * wait方法要求:
    61                           * 调用哪个对象的wait的方法 就要将该对象加锁
    62                           */
    63                          obj.wait();
    64                     }
    65                 } catch (InterruptedException e) {
    66                     // TODO Auto-generated catch block
    67                     e.printStackTrace();
    68                 }
    69                  
    70                 if(isFinish){
    71                     System.out.println("图片显示成功");
    72                 }else{System.out.println("图片显示失败");}
    73             }
    74          };
    75          download.start();
    76          show.start();
    77          
    78     }
    79  }

    7. 线程安全API与非线程安全API

    之前学习的API中就有设计为线程安全与非线程安全的类:
    StringBuffer 是同步的 synchronized append(); 安全的
    StringBuilder 不是同步的 append();
    相对而言StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的。当不存在并发时首选应当使用StringBuilder。
    同样的:
    Vector 和 Hashtable 是线程安全的
    ArrayList 和 HashMap则不是线程安全的。
    对于集合而言,Collections提供了几个静态方法,可以将集合或Map转换为线程安全的:
    例如:
    Collections.synchronizedList() :获取线程安全的List集合
    Collections.synchronizedMap():获取线程安全的Map
    ...
    List<String> list = new ArrayList<String>();
    list.add("A");
    list.add("B");
    list.add("C");
    list = Collections.synchronizedList(list);//将ArrayList转换为线程安全的集合
    System.out.println(list);//[A,B,C] 可以看出,原集合中的元素也得以保留
    ...

    /**
      * 转换线程安全的集合和Map
      */
     class SynCollectionAndMap{
        public static void main(String[] args) {
             //List集合
             List<String> list =new ArrayList<String>();
             list.add("a");
             list.add("b");
             list.add("c");
             //转换为线程安全的List集合
             list =Collections.synchronizedList(list);
             /*
              * 能保证:对集合元素进行操作的方法都是同步且
              *         互斥的。保证了线程的安全
              * 注意:在遍历的过程中,依然可以增删元素
              * 解决办法: 对遍历的代码片段加锁,锁的是集合这个对象
              */
             System.out.println(list);
             synchronized (list) {
                java.util.Iterator<String> it = list.iterator();
                while(it.hasNext()){
                    System.out.println(it.next());
                }
            }
             //Set集合
             Set<String> set =new HashSet<String>();
             set.add("a");
             set.add("b");
             set.add("c");
             //将Set集合转换为线程安全的
             set = Collections.synchronizedSet(set);
             System.out.println(set);
             
             
             Map<String,Integer> map =new HashMap<String, Integer>();
             map.put("张三",22);
             map.put("赵四",22);
             map.put("王五",22);
             //将Map转换为一个线程安全的
             map=Collections.synchronizedMap(map);
             System.out.println(map);
        }
     }
     

    8. 使用ExecutorService实现线程池

    当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。
    ExecutorService是java提供的用于管理线程池的类。

    线程池有两个主要作用:
    1.控制线程数量
    2.重用线程

    线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,
    服务完后不关闭该线程,而是将该线程还回到线程池中。
    在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务

    线程池有以下几种实现策略:
    Executors.newCachedThreadPool()
    创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
    Executors.newFixedThreadPool(int nThreads)
    创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
    Executors.newScheduledThreadPool(int corePoolSize)
    创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
    Executors.newSingleThreadExecutor()
    创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
    可以根据实际需求来使用某种线程池。例如,创建一个有固定线程数量的线程池:
    ...
    ExecutorService threadPool
    = Executors.newFixedThreadPool(30);//创建具有30个线程的线程池
    Runnable r1 = new Runable(){
    public void run(){
    //线程体
    }
    };
    threadPool.execute(r1);//将任务交给线程池,其会分配空闲线程来运行这个任务
    ...

    /**
      * 测试线程池
      * @author Administrator
      *
      */
     
     class TestThreadPoolDemo{
         public static void main(String[] args) {
            //创建了一个含有10个线程的线程池
             ExecutorService threadpool = Executors.newFixedThreadPool(2);
             for(int i=0;i<5;i++){
                 Handler handler =new Handler();
                 threadpool.execute(handler); 
             }
             System.out.println("任务全部指派完成");
        }
     }
     /*
      * 线程要执行的任务
      */
     class Handler implements Runnable{
         @Override
        public void run() {
             //获取运行当前任务的线程名字
             String name =Thread.currentThread().getName();
             System.out.println("运行当前任务的线程是:"+name);
             for(int i=0;i<10;i++){
                 System.out.println(name+":"+i);
                 try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
             }
             System.out.println("任务完毕");
        }
     }

    9. BlockingQueue双缓冲队列

    queue 一边进一边出
    Deque 两边都能进都能出

    BlockingQueue是双缓冲队列、BlockingDeque是双缓冲双端队列

    在多线程并发时,若需要使用队列,我们可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。
    BlockingQueue内部使用两条队列,可允许两个线程同时向队列一个做存储,一个做取出操作。在保证并发安全的同时提高了队列的存取效率。

    双缓冲队列有一下几种实现:
    ArrayBlockingDeque:规定大小的BlockingDeque,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的。

    LinkedBlockingDeque:大小不定的BlockingDeque,若其构造函数带一个规定大小的参数,生成的BlockingDeque有大小限制,若不带大小参数,所生成的BlockingDeque的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。

    PriorityBlockingDeque:类似于LinkedBlockDeque,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。

    SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。

    /**
      * 双缓冲队列
      */
     class TestBlockingQueueDemo{
         public static void main(String[] args) {
             /*
              * 双缓冲队列,创建一个固定长度的,里面存放10个元素
              * 该队列是单向的,遵循先进先出的原则
              */
            final BlockingQueue<Integer> queue =new ArrayBlockingQueue<Integer>(10);
            /*
             * 双缓冲双端队列,与单队列的区别在于,队列两端都可以进出队
             */
    //        BlockingDeque<Integer> Deque =new LinkedBlockingDeque<Integer>(10);
            
            //向队列中添加元素的线程
            Thread offerThread =new Thread(){
                @Override
                public void run() {
                    for(int i=0;i<20;i++){
                        //局部变量使用前必须初始化
                        boolean tf = false;
                        try {
                            /*
                             * 该方法允许我们设置一个延迟时间
                             * 在延迟时间之后还没放入 便返回false
                             */
                            tf=queue.offer(i,5,TimeUnit.SECONDS);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("添加元素"+i+":"+tf);
                    }
                }
            };
            
            offerThread.start();
            
            //从队列中取出元素的线程
            Thread pullThread=new Thread(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    for(int i=0;i<20;i++){
                        int num=0;
                        try {
                            num=queue.poll(5,TimeUnit.SECONDS);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        System.out.println("取出的元素是:"+num);
                    }
                }
            };
            
            
            pullThread.start();
            
        }
     }
     
  • 相关阅读:
    struts2第一天——入门和基本操作
    eclipse各种小图标含义
    复制web项目时注意修改web项目名
    AndroidCityPicker仿IOS选择效果
    每日五题(Spring)
    block-循环引用
    给EasyUi的Form加入自己主动填充部分输入框的方法
    智能停车O2O 独角兽初现:“ETCP停车”获5000万美金A轮融资
    解决移动端页面滚动后不触发touchend事件
    《从零開始学Swift》学习笔记(Day 61)——Core Foundation框架之内存管理
  • 原文地址:https://www.cnblogs.com/manue1/p/4492676.html
Copyright © 2020-2023  润新知