Java多线程之线程安全

Java多线程之线程安全

 

保证线程安全的方法

 

  1. Synchronized,同步方法,同步块
  2. Volatile,修饰变量
  3. Lock,可重入锁,读写锁
  4. 不变类,final class,很少使用
  5. 线程安全类
  6. 不使用共享变量
  7. ThreadLocal

 

 

线程安全类

 

  1. Vector、Hashtable,如果不涉及线程安全,用这些类就会浪费资源
  2. 用静态同步方法封装非同步集合
// Collections中有很多静态的同步方法,来同步那些非同步的集合
List list = Collections.synchronizedList(new ArrayList());

 

 

 

ThreadLocal

 

给每个线程一个变量拷贝,由线程自己来维护,导致每个线程间的这个变量相互独立,也就没有了共享变量,所以线程安全。

 

在高并发场景:如果不考虑延迟、共享数据,这会是个不错的选择。

 

 

 

不可变对象

 

不可变对象可以在没有同步的情况下共享,降低访问时的同步 开销。

 

创建不可变类

  • 全部变量都是私有的
  • 通过构造函数初始化私有变量
  • 没有setter方法,只有getter方法
  • getter方法不要直接返回对象本身,要返回克隆对象

 

 

 

线程安全单例

 

对于单例模式,我们需要注意的是,有一些单例的写法是线程安全的,而有些是非线程安全的。

 

饿汉单例(线程安全)

 

// 在类装载时就实例化
public class Singleton {
    private static Singleton sin=new Singleton();    ///直接初始化一个实例对象
    private Singleton(){    ///private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
    }
    public static Singleton getSin(){    ///该类唯一的一个public方法    
        return sin;
    }
}
 

 

懒汉单例(非线程安全)

 

// 在使用时再实例化
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}
 

 

加锁的懒汉单例(线程安全)

 

// 这种加锁的方式虽然能解决线程安全,但是影响性能
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}
 

 

双重校验锁的懒汉单例(线程安全)

 

// 保证了线程安全和性能
public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}
 

 

 

 

常用的同步类

 

  • CountDownLatch闭锁,运动员计时
  • Semaphone信号量,并发资源访问控制
  • CyclicBarrier栅栏,游戏中等待所有人到了,再进入下一关
  • Phaser:一种可重用的同步屏障,功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。
  • Exchanger:允许两个线程在某个汇合点交换对象,在某些管道设计时比较有用。

 

CountDownLatch

 

一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

 

主要方法

// 构造方法参数指定了计数的次数
public CountDownLatch(int count);

// 当前线程调用此方法,则计数减一
public void countDown();

// 调用此方法会一直阻塞当前线程,直到计时器的值为0
public void await() throws InterruptedException

 

代码

public class CountDownLatchDemo {  
    final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    public static void main(String[] args) throws InterruptedException {  
        CountDownLatch latch=new CountDownLatch(2);//两个工人的协作  
        Worker worker1=new Worker("zhang san", 5000, latch);  
        Worker worker2=new Worker("li si", 8000, latch);  
        worker1.start();//  
        worker2.start();//  
        latch.await();//等待所有工人完成工作  
        System.out.println("all work done at "+sdf.format(new Date()));  
    }  
      
      
    static class Worker extends Thread{  
        String workerName;   
        int workTime;  
        CountDownLatch latch;  
        public Worker(String workerName ,int workTime ,CountDownLatch latch){  
             this.workerName=workerName;  
             this.workTime=workTime;  
             this.latch=latch;  
        }  
        public void run(){  
            System.out.println("Worker "+workerName+" do work begin at "+sdf.format(new Date()));  
            doWork();//工作了  
            System.out.println("Worker "+workerName+" do work complete at "+sdf.format(new Date()));  
            latch.countDown();//工人完成工作,计数器减一  
  
        }  
          
        private void doWork(){  
            try {  
                Thread.sleep(workTime);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
      
       
} 

 

结果

Worker zhang san do work begin at 2011-04-14 11:05:11

Worker li si do work begin at 2011-04-14 11:05:11

Worker zhang san do work complete at 2011-04-14 11:05:16

Worker li si do work complete at 2011-04-14 11:05:19

all work done at 2011-04-14 11:05:19

 

 

Semaphore

 

厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

 

主要方法

// 构造函数,定义只有5个信号量
Semaphore(5)

// 获取一个许可,如果没有就等待
acquire()

// 释放一个许可
release()

 

代码

package com.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class TestSemaphore {

    public static void main(String[] args) {

        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();

        // 只能5个线程同时访问
        final Semaphore semp = new Semaphore(5);
        
        // 模拟20个客户端访问
        for (int index = 0; index < 20; index++) {

            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {

                    try {
                            // 获取许可
                            semp.acquire();
                            System.out.println("Accessing: " + NO);
                            Thread.sleep((long) (Math.random() * 10000));
                            // 访问完后,释放
                            semp.release();
                            System.out.println("-----------------"+semp.availablePermits());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
    
                    }
                }

            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();
       }
}

 

结果

Accessing: 0

Accessing: 1

Accessing: 3

Accessing: 4

Accessing: 2

-----------------0

Accessing: 6

-----------------1

Accessing: 7

-----------------1

Accessing: 8

-----------------1

Accessing: 10

-----------------1

Accessing: 9

-----------------1

Accessing: 5

-----------------1

Accessing: 12

-----------------1

Accessing: 11

-----------------1

Accessing: 13

-----------------1

Accessing: 14

-----------------1

Accessing: 15

-----------------1

Accessing: 16

-----------------1

Accessing: 17

-----------------1

Accessing: 18

-----------------1

Accessing: 19

 

CyclicBarrier

循环障栅栏,一组线程写操作,并且只有所有线程完成写操作,才继续做后面的事

常用方法

// 构造函数,定义三个线程
CyclicBarrier(3);

// 当所有参与者都调用await()之前,前面执行完的线程一直等待
await()

 

代码

public class CyclicBarrierTest {  
  
    public static void main(String[] args) throws IOException, InterruptedException {  
        //如果将参数改为4,但是下面只加入了3个选手,这永远等待下去  
        //Waits until all parties have invoked await on this barrier.   
        CyclicBarrier barrier = new CyclicBarrier(3);  
  
        ExecutorService executor = Executors.newFixedThreadPool(3);  
        executor.submit(new Thread(new Runner(barrier, "1号选手")));  
        executor.submit(new Thread(new Runner(barrier, "2号选手")));  
        executor.submit(new Thread(new Runner(barrier, "3号选手")));  
  
        executor.shutdown();  
    }  
}  
  
class Runner implements Runnable {  
    // 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)  
    private CyclicBarrier barrier;  
  
    private String name;  
  
    public Runner(CyclicBarrier barrier, String name) {  
        super();  
        this.barrier = barrier;  
        this.name = name;  
    }  
  
    @Override  
    public void run() {  
        try {  
            Thread.sleep(1000 * (new Random()).nextInt(8));  
            System.out.println(name + " 准备好了...");  
            // barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。  
            barrier.await();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } catch (BrokenBarrierException e) {  
            e.printStackTrace();  
        }  
        System.out.println(name + " 起跑!");  
    }  
}

 

结果

3号选手 准备好了...

2号选手 准备好了...

1号选手 准备好了...

1号选手 起跑!

2号选手 起跑!

3号选手 起跑!

为什么只有ConcurrentHashMap,没有Concurrent的List?

       从上面的描述,可以体现出Concurrent 比 CopyOnWrite要好,因为保证线程安全和并发瓶颈,那为什么List没有Concurrent实习那呢?

原因

       Concurrent的List很难实现,例如:contains() 这样一个操作来说,当你进行搜索的时候如何避免锁住整个list?

       同时,这也与ConcurrentHashMap的实现方式有关,一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。

     同理,由于HashTable的value不能为null,所以就没有ConcurrentHashSet。

猜你喜欢

转载自youyu4.iteye.com/blog/2352540