文章目录
关于线程同步
因为Java支持多线程并发控制,而多线程往往会导致在多线程中共享的资源出现不同步的问题,因此如何实现数据的同步就显得尤为重要了。
以下为常用的同步方式
- 使用synchronized关键字
- 使用特殊域变量(volatile)实现线程同步
- 使用重入锁实现线程同步
- 使用局部变量实现线程同步
- 使用阻塞队列实现线程同步
- 使用原子变量实现线程同步
一、使用synchronized关键字
Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
声明sybchronized方法的方式
synchronized void fun(){
/** **/
}
synchronized修饰代码块方式
public void fun(int count){
synchronized (this) {
count ++;
}
System.out.println(count);
}
所有对象都自动含有单一的锁(也称监视器),当在对象上调用任意synchronized方法的时候,此对象会被加锁。
锁也分为对象锁和类锁,两者是不同锁,是异步的
- 对象锁
对象锁是指加在普通方法和this上
synchronized (this) { // 注意this做为监视器.它与class分别是二个不同监视器.不会存在class被获取,this就要等的现象.这也是我以前关于监视器的一个误区.
for (int i = 0; i < 100; i++) {
System.out.println("testSyncBlock:" + i);
}
}
- 类锁
synchronized(class)很特别,它会让另一个线程在任何需要获取class做为monitor的地方等待。
synchronied修饰的静态方法和class属于类锁
synchronized (RunnableTest.class) { // 显示使用获取class做为监视器.它与static synchronized method隐式获取class监视器一样.
for (int i = 0; i < 100; i++) {for (int i = 0; i < 100; i++) {
System.out.println("testSyncBlock:" + i);System.out.println("testSyncBlock:" + i);
}
}
//修饰静态方法
//同步方法
public synchronized static void save(int count) {
account++;
}
二、使用特殊域变量volatile实现线程同步
关于共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
- volatile关键字为域变量的访问提供了一种免锁机制,
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
例子1不使用volatile关键字修饰
//线程1
boolean stop = false;
while(!stop){
/**
**/
}
//线程2
stop = true;
线程1大部分情况下会被中断,但也会出现并不会中断的情况。每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
例子2使用volatile关键字修饰
//线程1
volatile boolean stop = false;
while(!stop){
/**
**/
}
//线程2
stop = true;
当使用了volatile修饰stop后,必定能中断线程1了,因为用volatile关键字修饰后的变量具有以下特性:
-
使用volatile关键字会强制将修改的值立即写入主存;
-
使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
-
由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1读取到的就是最新的正确的值。
三、 使用重入锁实现线程同步
Java SE5的java.util.concurrent类库还包含有定义再java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式地创建、锁定和释放。因此 ,它和内建的锁形式相比,代码缺乏优雅性。但是,对于解决某些类型的问题来说,它更加灵活。
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
private int count = 0;
//声明锁
private Lock lock = new ReentranlLock();
public int getCount(){
lock.lock();
try{
count++;
return count;
}finally{
lock.unlock();
}
}
在该实例中添加一个被互斥调用的锁,并使用lock()和unlock()方法在getCount()内部创建了临界资源。
与synchronized关键字对比:
尽管try-finally所需的代码比synchronized关键字要多,但这代表了显示的Lock对象的优点之一。如果使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常。但没有机会去做清理工作。有了显示Lock对象,你就可以使用finally子句将系统维护在正确的状态了。
四、 使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal 类的常用方法
- ThreadLocal() : 创建一个线程本地变量
- get() : 返回此线程局部变量的当前线程副本中的值
- initialValue() : 返回此线程局部变量的当前线程的"初始值"
- set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
public class Computer{
//使用ThreadLocal类管理共享变量count
private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(){
count.set(++count.get());
}
public int getCount(){
return count.get();
}
}
五、 使用阻塞队列实现线程同步
上面关于同步的实现方式是Java并发程序设计基础的底层构建块,在实际的编程使用中,使用较高层次的类库会相对安全方便。对于典型的生产者和消费者问题,可以使用阻塞队列解决,这样就不用考虑锁和条件的问题了。
生产者线程向队列插入元素,消费者线程从队列取出元素。当添加时队列已满或取出时队列为空,阻塞队列导致线程阻塞。将阻塞队列用于线程管理工具时,主要用到put()和take()方法。对于offer()、poll()、peek()方法不能完成时,只是给出一个错误提示而不会抛出异常。
java中提供了java.util.concurrent.BlockingQueue接口的以下几种实现:
(1)ArrayBlockingQueue:使用数组实现阻塞队列,必须指定一个容量或者可选的公平性来构造。
(2)LinkedBlockingQueue:使用链表实现,可以创建不受限的或受限的队列。
(3)PriorityBlockingQueue:优先队列,可以创建不受限的或受限的优先队列。
注:对于不受限的队列,put方法永远不会阻塞。
阻塞队列和前面说的同步和锁的不同之处在于,阻塞队列中实现了锁和同步,所以不用手动编码。
六、 使用原子变量实现线程同步
Java SE5引入了诸如AtomicInteger、AtomicLong、AtomicReference的特殊的原子性变量,它们提供下面形式的原子性更新操作
boolean compareAndSet(expectedValue,updateValue);
这些类的原子性是机器级别上的原子性,在日常编程中使用较少。但在性能调优上,就大有用武之地。
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadCommunicate {
static class Counter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.getAndIncrement();
}
public void decrement() {
c.getAndDecrement();
}
public int value() {
return c.get();
}
}
static class IncrementTask implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
counter.increment();
System.out.println("increment"+counter.value());
}
}
}
static class DecrementTask implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
counter.decrement();
System.out.println("decrement"+counter.value());
}
}
}
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread i = new Thread(new IncrementTask());
Thread d = new Thread(new DecrementTask());
i.start();
d.start();
}
}
getAndIncrement() 即自增1,getAndDecrement()自减1’
最后程序输出的是
increment1
increment2
increment3
increment4
increment5
increment6
increment7
increment8
increment9
increment10
decrement9
decrement8
decrement7
decrement6
decrement5
decrement4
decrement3
decrement2
decrement1
decrement0
tomicInteger类中的方法能保证对内存中的int值的操作都是原子性的,换句话说就能保证一个线程在对int操作的过程中不会被另一个线程打断。
注意:
Atomic 类被设计用来构建java.util.concurrent中的类,因此只有特殊情况下才使用它们,但不确保出现其他问题,一般依赖于锁要更安全一些。
参考资料
[1] https://www.cnblogs.com/XHJT/p/3897440.html
[2] java编程思想
[3] Java并发编程:volatile关键字解析
[4]ThreadLocal详解(实现多线程同步访问变量)
[5]Java多线程之同步与阻塞队列
[6]Java多线程复习与巩固(八)–原子性操作与原子变量