线程的同步是为了防止多个线程访问一个数据对象时,对数据造成破坏。在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。
Java中线程执行步骤:
- 清空工作内存;
- 从主内存拷贝对象副本到工作内存;
- 执行代码(计算或者输出等);
- 刷新主内存数据;
Java线程的两个特性,可见性和有序性。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。
1 2 3 4 5 6 7
| public class Count { private int num = 0; public void count(){ num++; } }
|
假如在多个线程之间共享了Count类的一个对象,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程栈),工作内存存储了主内存Count对象的一个副本,当线程操作count对象时,首先从主内存复制count对象到工作内存中,然后执行代码count.count(),改变num值,最后用工作内存count刷新主内存count。
- 可见性: 当一个对象在多个内存中都存在副本时,如果一个内存修改了共享变量,其它线程也应该能够看到被修改后的值。
- 有序性: 多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程,一个最经典的例子就是银行汇款问题,一个银行账户存款100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操作,并将数据刷新到主内存,最后主内存数据100+10=110,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后取款,此为有序性。
synchronized
同步方法
使用synchronized关键字修饰的方法
Java的每个对象都有一个内置锁,当调用synchronized修饰的方法时,需要先获得内置锁,否则线程就处于阻塞状态。内置锁会保护整个方法,方法执行完成之后线程释放该内置锁。所以,同一时刻,对象的synchronized方法只能有一个处于执行状态。
1 2
| public synchronized void modify(){ }
|
synchronized 关键字也可以修饰静态方法,此时获取的是整个类对象的锁,XXX.class;
1 2 3 4 5
| public static synchronized void add() {} public static void add() { synchronized(XXX.class){} }
|
同步代码块
由synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动被加上锁,从而实现同步,此时的锁就是 synchronized 后面所锁定的对象的内置锁;
同步是一种高开销的操作,因此应该尽量减少同步的内容,通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
死锁
两个线程相互等待锁都进入阻塞状态,则发生死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| private static void deadLock() throws Exception { synchronized (mLock) { System.out.println("OuterLock"); Future<String> fu = mExecutor.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("InnerLock"); synchronized (mLock) { System.out.println("InnerLockGetted"); } return "NeverRetunedValue"; } }); String rs = fu.get(); System.out.println("OuterLockWillUnLock: " + rs); } }
|
锁对象
每个锁对象(JLS中叫monitor)都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒(notify)后,才会进入到就绪队列,等待CPU的调度,反之,当一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒。当第一个线程执行输出方法时,获得同步锁,执行输出方法,恰好此时第二个线程也要执行输出方法,但发现同步锁没有被释放,第二个线程就会进入就绪队列,等待锁被释放。
synchronized 既保证了多线程的并发有序性,又保证了多线程的内存可见性。
Lock
1 2 3 4 5 6 7 8 9 10 11 12
| private Lock lock = new ReentrantLock(); public void add(float v) { try { lock.lock(); this.money += v; System.out.println(this.getMoney()); } finally { lock.unlock(); } }
|
注意:用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。
ReentrantLock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class T1 extends Thread { ReentrantLock l; public T1(ReentrantLock l) { this.l = l; } @Override public void run() { System.out.println(this + " TryLock------------------------"); l.lock(); System.out.println(this + " Locked....: " + System.currentTimeMillis()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } l.unlock(); System.out.println(this + " AfterLock: " + System.currentTimeMillis()); } }
|
- lock()方法用于获取锁对象,如果获取到了锁对象,则线程继续向下执行,反之则线程进入等待阻塞状态。
- unlock()方法用于释放锁对象,锁被释放后,其它处于阻塞状态的线程将获取到锁对象。
- 和synchronized不同的是,多个由于调用lock()方法进入阻塞状态的线程,将根据调用lock()方法的时间先后顺序获取到锁,而synchronized则是无序的。
- lock()、unlock()通常用于比较复杂的业务场景,在线程池(ThreadPoolExecutor)中有使用到。
ReadWriteLock
在对数据进行读写的时候,为了保证数据的一致性和完整性,需要读和写是互斥的,写和写是互斥的,但是读和读是不需要互斥,这样读和读不互斥性能就会更高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| private static class Data { private int age = 22; private ReadWriteLock lock = new ReentrantReadWriteLock(); private Random random = new Random(); public void setAge(int age, int index) { try { lock.writeLock().lock(); Thread.sleep(200 * index); this.age = age; } catch (Exception e) { } finally { print("-- endSetAge..." + Thread.currentThread().getName() + " : " + this.age); lock.writeLock().unlock(); } } public int getAge() { try { lock.readLock().lock(); Thread.sleep(random.nextInt(500)); } catch (Exception e) { } finally { lock.readLock().unlock(); print(">> endGetAge..." + Thread.currentThread().getName() + " : " + this.age); return this.age; } } }
|
模拟线程调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public void main(String[] args) { final Data data = new Data(); final Random random = new Random(); for (int i = 0; i < 5; i++) { final int o = i; new Thread() { @Override public void run() { try { Thread.sleep(random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } data.setAge(80 + o, (o + 1)); } }.start(); new Thread() { @Override public void run() { try { Thread.sleep(random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } data.getAge(); } }.start(); } }
|
锁对象Lock
其它
- 只能同步方法,而不能同步变量和类;
- 每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
- 不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
- 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
- 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
- 线程睡眠时,它所持的任何锁都不会释放。
- 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
- 同步损害并发性,应该尽可能缩小同步范围。同步除了同步整个方法,还可以同步方法中一部分代码块。
- 在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
- 当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。
文章开头提到了线程执行步骤,在加锁之后,步骤可以简述为:
- 获得同步锁;
- 清空工作内存;
- 从主内存拷贝对象副本到工作内存;
- 执行代码(计算或者输出等);
- 刷新主内存数据;
- 释放同步锁;
参考
Java线程同步与锁
Java线程同步synchronized