线程同步与锁

Java线程:线程的同步与锁

一. 什么是同步

  同步操作:有先后顺序的,相当于你干完我再干

  异步操作:多线程并发操作,相当于各干各的

二、同步的目的

  线程的同步是为了防止多个线程访问同一个数据(临界资源)时,对数据造成的破坏。

  常见的临界资源:

    1)多线程共享实例变量

    2)多线程共享静态公共变量

三、锁机制

  1、锁的原理

    Java中每个对象都有一个内置锁,当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁

    也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

    当程序运行到synchronized同步方法或代码块时该对象锁才起作用。

    一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的

    synchronized方法或代码块,直到该锁被释放。

    释放锁是指持锁线程退出了synchronized同步方法或代码块。

  2. 同步和互斥的区别

    同步:A要继续执行需要B完成某一个操作操作才能继续进行。是指在互m数情况是指可以允许多个访问者同时访问资源。

    互斥:A访问了资源B就不能去访问 必须等A访问完了才行。是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访

       问顺序,即访问是无序的。

    可以理解是一致的概念。

四.  如何实现线程同步

    在Java语言中,每个对象都有一个锁,一个线程可以通过关键字synchronized(互斥锁)来申请获取某个对象的锁,关键字synchronized可以被用于

      1)方法(粗粒度锁,对性能影响较大)

          粗粒度锁:覆盖多个对象的单个锁.

               1)简化了加锁行为

               2)且不必为了给它们加锁而加载所有对象

      或2)代码块(细粒度锁)级别。

          细粒度锁:synchronized,ReentrantLock,ReentrantReadWriteLock已基本可以满足编程需求,但其粒度都太大,同一时刻只有一个线程能进入同步

               块,这对于某些高并发的场景并不适用。下面实现了一个基于KEY(主键)的互斥锁,具有更细的粒度,在缓存或其他基于KEY的场景中有很大的

               用处。下面将讲解这个锁的设计和实现

 设想这么一个场景:转账

1 private int[] accounts; // 账户数组,其索引为账户ID,内容为金额  
2   
3 public boolean transfer(int from, int to, int money) {  
4     if (accounts[from] < money)  
5         return false;  
6     accounts[from] -= money;  
7     accounts[to] += money;  
8     return true;  
9 }  

从from中转出金额到to中。可能同时会有很多个线程同时调用这个转账方法,为保证原子性,保证金额不会出错,必须为这个方法加个锁,防止对共享变量accounts的并发修改。

加锁后的代码如下:

 1 private int[] accounts; // 账户数组,其索引为账户ID,内容为金额  
 2 private Lock lock = new ReentrantLock();  
 3   
 4 public boolean transfer(int from, int to, int money) {  
 5     lock.lock();  
 6     try {  
 7         if (accounts[from] < money)  
 8             return false;  
 9         accounts[from] -= money;  
10         accounts[to] += money;  
11         return true;  
12     } finally {  
13         lock.unlock();  
14     }  
15 }  

好了,加锁后这个代码就能保证金额不出错了。但问题又出现了,一次只能执行一个转账过程!意思就是A给B转账的时候,C要给D转账也得等A给B转完了才能开始转。这就有点扯蛋了,就像只有一个柜台,所有人必须排队等前面的处理完了才能到自己,效率太低。

解决这种情况有一个方案:A给B转账的时候只锁定A和B的账户,使其转账期间不能再有其他针对A和B账户的操作,但其他账户的操作可以并行发生。类似于如下场景:

 1 public boolean transfer(int from, int to, int money) {  
 2     lock.lock(from, to);  
 3     try {  
 4         if (accounts[from] < money)  
 5             return false;  
 6         accounts[from] -= money;  
 7         accounts[to] += money;  
 8         return true;  
 9     } finally {  
10         lock.unlock(from, to);  
11     }  
12 }  

但很显然,JAVA并没有为我们提供这样的锁(也有可能是我没找到。。。)

于是,就在这样的需求下我花了整一天来实现了这个锁——KeyLock(代码量很短,但多线程的东西真的很让人头疼)

不同于synchronized等锁,KeyLock是对所需处理的数据的KEY(主键)进行加锁,只要是对不同key操作,其就可以并行处理,大大提高了线程的并行度(最后有几个锁的对比测试)

总结下就是:对相同KEY操作的线程互斥,对不同KEY操作的线程可以并行

KeyLock有如下几个特性:

    1、细粒度,高并行性
    2、可重入
    3、公平锁
    4、加锁开销比ReentrantLock大,适用于处理耗时长、key范围大的场景

 

KeyLock代码如下(注释很少,因为我也不知道该怎么写清楚,能看懂就看,懒得看的直接用就行):

 1 public class KeyLock<K> {  
 2     // 保存所有锁定的KEY及其信号量  
 3     private final ConcurrentMap<K, Semaphore> map = new ConcurrentHashMap<K, Semaphore>();  
 4     // 保存每个线程锁定的KEY及其锁定计数  
 5     private final ThreadLocal<Map<K, LockInfo>> local = new ThreadLocal<Map<K, LockInfo>>() {  
 6         @Override  
 7         protected Map<K, LockInfo> initialValue() {  
 8             return new HashMap<K, LockInfo>();  
 9         }  
10     };  
11   
12     /** 
13      * 锁定key,其他等待此key的线程将进入等待,直到调用{@link #unlock(K)} 
14      * 使用hashcode和equals来判断key是否相同,因此key必须实现{@link #hashCode()}和 
15      * {@link #equals(Object)}方法 
16      *  
17      * @param key 
18      */  
19     public void lock(K key) {  
20         if (key == null)  
21             return;  
22         LockInfo info = local.get().get(key);  
23         if (info == null) {  
24             Semaphore current = new Semaphore(1);  
25             current.acquireUninterruptibly();  
26             Semaphore previous = map.put(key, current);  
27             if (previous != null)  
28                 previous.acquireUninterruptibly();  
29             local.get().put(key, new LockInfo(current));  
30         } else {  
31             info.lockCount++;  
32         }  
33     }  
34       
35     /** 
36      * 释放key,唤醒其他等待此key的线程 
37      * @param key 
38      */  
39     public void unlock(K key) {  
40         if (key == null)  
41             return;  
42         LockInfo info = local.get().get(key);  
43         if (info != null && --info.lockCount == 0) {  
44             info.current.release();  
45             map.remove(key, info.current);  
46             local.get().remove(key);  
47         }  
48     }  
49   
50     /** 
51      * 锁定多个key 
52      * 建议在调用此方法前先对keys进行排序,使用相同的锁定顺序,防止死锁发生 
53      * @param keys 
54      */  
55     public void lock(K[] keys) {  
56         if (keys == null)  
57             return;  
58         for (K key : keys) {  
59             lock(key);  
60         }  
61     }  
62   
63     /** 
64      * 释放多个key 
65      * @param keys 
66      */  
67     public void unlock(K[] keys) {  
68         if (keys == null)  
69             return;  
70         for (K key : keys) {  
71             unlock(key);  
72         }  
73     }  
74   
75     private static class LockInfo {  
76         private final Semaphore current;  
77         private int lockCount;  
78   
79         private LockInfo(Semaphore current) {  
80             this.current = current;  
81             this.lockCount = 1;  
82         }  
83     }  
84 }  

KeyLock使用示例:

 1 private int[] accounts;  
 2 private KeyLock<Integer> lock = new KeyLock<Integer>();  
 3   
 4 public boolean transfer(int from, int to, int money) {  
 5     Integer[] keys = new Integer[] {from, to};  
 6     Arrays.sort(keys); //对多个key进行排序,保证锁定顺序防止死锁  
 7     lock.lock(keys);  
 8     try {  
 9         //处理不同的from和to的线程都可进入此同步块  
10         if (accounts[from] < money)  
11             return false;  
12         accounts[from] -= money;  
13         accounts[to] += money;  
14         return true;  
15     } finally {  
16         lock.unlock(keys);  
17     }  
18 }  

好,工具有了,接下来就是测试了,为了测出并行度,我把转账过程延长了,加了个sleep(2),使每个转账过程至少要花2毫秒(这只是个demo,真实环境下对数据库操作也很费时)。

测试代码如下:

  1 //场景:多线程并发转账  
  2 public class Test {  
  3     private final int[] account; // 账户数组,其索引为账户ID,内容为金额  
  4   
  5     public Test(int count, int money) {  
  6         account = new int[count];  
  7         Arrays.fill(account, money);  
  8     }  
  9   
 10     boolean transfer(int from, int to, int money) {  
 11         if (account[from] < money)  
 12             return false;  
 13         account[from] -= money;  
 14         try {  
 15             Thread.sleep(2);  
 16         } catch (Exception e) {  
 17         }  
 18         account[to] += money;  
 19         return true;  
 20     }  
 21       
 22     int getAmount() {  
 23         int result = 0;  
 24         for (int m : account)  
 25             result += m;  
 26         return result;  
 27     }  
 28   
 29     public static void main(String[] args) throws Exception {  
 30         int count = 100;        //账户个数  
 31         int money = 10000;      //账户初始金额  
 32         int threadNum = 8;      //转账线程数  
 33         int number = 10000;     //转账次数  
 34         int maxMoney = 1000;    //随机转账最大金额  
 35         Test test = new Test(count, money);  
 36           
 37         //不加锁  
 38 //      Runner runner = test.new NonLockRunner(maxMoney, number);  
 39         //加synchronized锁  
 40 //      Runner runner = test.new SynchronizedRunner(maxMoney, number);  
 41         //加ReentrantLock锁  
 42 //      Runner runner = test.new ReentrantLockRunner(maxMoney, number);  
 43         //加KeyLock锁  
 44         Runner runner = test.new KeyLockRunner(maxMoney, number);  
 45           
 46         Thread[] threads = new Thread[threadNum];  
 47         for (int i = 0; i < threadNum; i++)  
 48             threads[i] = new Thread(runner, "thread-" + i);  
 49         long begin = System.currentTimeMillis();  
 50         for (Thread t : threads)  
 51             t.start();  
 52         for (Thread t : threads)  
 53             t.join();  
 54         long time = System.currentTimeMillis() - begin;  
 55         System.out.println("类型:" + runner.getClass().getSimpleName());  
 56         System.out.printf("耗时:%dms\n", time);  
 57         System.out.printf("初始总金额:%d\n", count * money);  
 58         System.out.printf("终止总金额:%d\n", test.getAmount());  
 59     }  
 60   
 61     // 转账任务  
 62     abstract class Runner implements Runnable {  
 63         final int maxMoney;  
 64         final int number;  
 65         private final Random random = new Random();  
 66         private final AtomicInteger count = new AtomicInteger();  
 67   
 68         Runner(int maxMoney, int number) {  
 69             this.maxMoney = maxMoney;  
 70             this.number = number;  
 71         }  
 72   
 73         @Override  
 74         public void run() {  
 75             while(count.getAndIncrement() < number) {  
 76                 int from = random.nextInt(account.length);  
 77                 int to;  
 78                 while ((to = random.nextInt(account.length)) == from)  
 79                     ;  
 80                 int money = random.nextInt(maxMoney);  
 81                 doTransfer(from, to, money);  
 82             }  
 83         }  
 84   
 85         abstract void doTransfer(int from, int to, int money);  
 86     }  
 87   
 88     // 不加锁的转账  
 89     class NonLockRunner extends Runner {  
 90         NonLockRunner(int maxMoney, int number) {  
 91             super(maxMoney, number);  
 92         }  
 93   
 94         @Override  
 95         void doTransfer(int from, int to, int money) {  
 96             transfer(from, to, money);  
 97         }  
 98     }  
 99   
100     // synchronized的转账  
101     class SynchronizedRunner extends Runner {  
102         SynchronizedRunner(int maxMoney, int number) {  
103             super(maxMoney, number);  
104         }  
105   
106         @Override  
107         synchronized void doTransfer(int from, int to, int money) {  
108             transfer(from, to, money);  
109         }  
110     }  
111   
112     // ReentrantLock的转账  
113     class ReentrantLockRunner extends Runner {  
114         private final ReentrantLock lock = new ReentrantLock();  
115   
116         ReentrantLockRunner(int maxMoney, int number) {  
117             super(maxMoney, number);  
118         }  
119   
120         @Override  
121         void doTransfer(int from, int to, int money) {  
122             lock.lock();  
123             try {  
124                 transfer(from, to, money);  
125             } finally {  
126                 lock.unlock();  
127             }  
128         }  
129     }  
130   
131     // KeyLock的转账  
132     class KeyLockRunner extends Runner {  
133         private final KeyLock<Integer> lock = new KeyLock<Integer>();  
134   
135         KeyLockRunner(int maxMoney, int number) {  
136             super(maxMoney, number);  
137         }  
138   
139         @Override  
140         void doTransfer(int from, int to, int money) {  
141             Integer[] keys = new Integer[] {from, to};  
142             Arrays.sort(keys);  
143             lock.lock(keys);  
144             try {  
145                 transfer(from, to, money);  
146             } finally {  
147                 lock.unlock(keys);  
148             }  
149         }  
150     }  
151 }  

最最重要的测试结果:

(8线程对100个账户随机转账总共10000次):

       类型:NonLockRunner(不加锁)
       耗时:2482ms
       初始总金额:1000000
       终止总金额:998906(无法保证原子性)

       类型:SynchronizedRunner(加synchronized锁)
       耗时:20872ms
       初始总金额:1000000
       终止总金额:1000000

       类型:ReentrantLockRunner(加ReentrantLock锁)
       耗时:21588ms
       初始总金额:1000000
       终止总金额:1000000

       类型:KeyLockRunner(加KeyLock锁)
       耗时:2831ms
       初始总金额:1000000
       终止总金额:1000000

 

    锁定方法往往不是一个很好的选择,取而代之的我们应该只锁定那些访问共享资源的代码块,因为每一个对象都有一个锁,

    所以可以通过创建虚拟对象来实现代码块级别的同步,方法块级别的锁比锁定整个方法更有效。

   

    

  

关于锁和同步,有以下几个要点:

1)、只能同步方法,而不能同步变量和类;

2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?

3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。

4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。

6)、线程睡眠时,它所持的任何锁都不会释放。

7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。

8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。

9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:

    public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }

 

当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:

    public synchronized int getX() {
        return x++;
    }

与

    public int getX() {
        synchronized (this) {
            return x;
        }
    }

效果是完全一样的。

3)静态方法同步

要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。

例如:

public static synchronized int setName(String name){

      Xxx.name = name;

}

等价于
public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

 

 1 /**
 2  * 静态方法若使用synchronized修饰后,那么该方法
 3  * 一定具有同步效果。
 4  * 静态方法的同步监视器对象为当前类的类对象。
 5  * 类对象:Class类型的实例。
 6  * 每个类在被JVM加载时,JVM都会创建一个且只创建
 7  * 一个Class类型的实例来表示它。所以,每个类在
 8  * JVM内部都有唯一的一个Class类型的实例对应,而
 9  * 静态方法就是将该Class的实例上锁的。
10  * @author adminitartor
11  *
12  */
13 public class SyncDemo3 {
14     public static void main(String[] args) {
15         Thread t1 = new Thread(){
16             public void run(){
17                 Foo.dosome();
18             }
19         };
20         Thread t2 = new Thread(){
21             public void run(){
22                 Foo.dosome();
23             }
24         };
25         t1.start();
26         t2.start();
27     }
28 }
29 
30 class Foo{
31     public synchronized static void dosome(){
32         try {
33             Thread t = Thread.currentThread();
34             System.out.println(t.getName()+":正在运行dosome方法...");
35             Thread.sleep(5000);            
36             System.out.println(t.getName()+":运行dosome方法完毕!");
37         } catch (Exception e) {
38             e.printStackTrace();
39         }
40     }
41 }

 

五、如果线程不能获得锁会怎么样

 

如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种池中,必须在哪里等待,直到其锁被释放,该线程再次变为可运行或运行为止。

当考虑阻塞时,一定要注意哪个对象正被用于锁定:

1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。

2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

 

六、线程安全类

 

当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。

即使是线程安全类,也应该特别小心,因为操作的线程是间仍然不一定安全。

举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。

看个代码:

 

publicclass NameList {
    private List nameList = Collections.synchronizedList(new LinkedList());

    publicvoid add(String name) {
        nameList.add(name); 
    } 

    public String removeFirst() {
        if (nameList.size() > 0) {
            return (String) nameList.remove(0);
        } else {
            returnnull;
        } 
    } 
}

 

publicclass Test {
    publicstaticvoid main(String[] args) {
        final NameList nl =new NameList(); 
        nl.add("aaa");
        class NameDropperextends Thread{ 
            publicvoid run(){
                String name = nl.removeFirst(); 
                System.out.println(name); 
            } 
        } 

        Thread t1 = new NameDropper();
        Thread t2 = new NameDropper();
        t1.start(); 
        t2.start(); 
    } 
}

 

虽然集合对象

    private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但是程序还不是线程安全的。

出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。

 

解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:

publicclass NameList {
    private List nameList = Collections.synchronizedList(new LinkedList());

    publicsynchronizedvoid add(String name) {
        nameList.add(name); 
    } 

    publicsynchronized String removeFirst() {
        if (nameList.size() > 0) {
            return (String) nameList.remove(0);
        } else {
            returnnull;
        } 
    } 
}

 

这样,当一个线程访问其中一个同步方法时,其他线程只有等待。

 

七、线程死锁

 

死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。

 

还是看一个比较直观的死锁例子:

 

publicclass DeadlockRisk {
    privatestaticclass Resource {
        publicint value;
    } 

    private Resource resourceA =new Resource(); 
    private Resource resourceB =new Resource(); 

    publicint read() {
        synchronized (resourceA) {
            synchronized (resourceB) {
                return resourceB.value + resourceA.value;
            } 
        } 
    } 

    publicvoid write(int a,int b) { 
        synchronized (resourceB) {
            synchronized (resourceA) {
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}

 

假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚持等待的话就出现死锁。

 

实际上,上面这个例子发生死锁的概率很小。因为在代码内的某个点,CPU必须从读线程切换到写线程,所以,死锁基本上不能发生。

 

但是,无论代码中发生死锁的概率有多小,一旦发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预定义的顺序获取锁这一策略。已经超出SCJP的考试范围。

 

八、线程同步小结

 

1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。

2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。

3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

4、对于同步,要时刻清醒在哪个对象上同步,这是关键。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

posted @ 2017-04-16 00:49  等你,在雨中  阅读(195)  评论(0)    收藏  举报