线程同步

 线程同步

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();
        
    }
 }
 
原文地址:https://www.cnblogs.com/manue1/p/4492676.html