Java线程同步的几种方法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34261214/article/details/82760295

关于线程同步

因为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 = falsewhile(!stop){
		/** 
		**/
	}
	//线程2
	stop = true

线程1大部分情况下会被中断,但也会出现并不会中断的情况。每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

例子2使用volatile关键字修饰

	//线程1
	volatile boolean stop = falsewhile(!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多线程复习与巩固(八)–原子性操作与原子变量

猜你喜欢

转载自blog.csdn.net/qq_34261214/article/details/82760295