Java中多线程访问冲突的解决方式
当时用多线程访问同一个资源时,非常容易出现线程安全的问题,例如当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失。因此需要采用同步机制来解决这种问题。
第一种 同步方法
第二种 同步代码块
第三种 使用特殊成员变量(volatile 成员变量)实现线程同步(前提是对成员变量的操作是原子操作)
第四种 使用Lock接口(java.util.concurrent.locks包)
第五种 使用线程局部变量(thread-local)解决多线程对同一变量的访问冲突,而不能实现同步(ThreadLocal类)
第六种 使用阻塞队列实现线程同步(java.util.concurrent包)
第七种 使用原子变量实现线程同步 (java.util.concurrent.atomic包)
第一种 同步方法
同步方法即使用 synchronized关键字修饰的方法。在Java语言中,每个对象都有一个内置的对象锁与之相关联,该锁会保护整个方法,即对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段synchronized代码时,首先需要获得这个锁,然后去执行相应的代码,执行结束,释放锁。synchronized关键字也可以以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
synchronized关键字主要有两种用法:synchronized方法和synchronized块。此外该关键字还可以作用于静态方法、类或某个实例,但这都对程序的效率有很大的影响。
给一个方法增加synchronized关键字之后就可以使它成为同步方法,这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的抽象方法。
synchronized方法,在方法的声明前加入synchronized关键字。例如
1 package com.test.multiThread; 2 3 public class Bank { 4 private int account = 0; 5 6 public int getAccount(){ 7 return account; 8 } 9 // 同步方法 10 public synchronized void save(int money){ 11 this.account += money; 12 } 13 } 14 15 ================================= 16 17 package com.test.multiThread; 18 19 public class MyThread implements Runnable { 20 private Bank bank; 21 public MyThread(Bank bank){ 22 this.bank = bank; 23 } 24 @Override 25 public void run() { 26 bank.save(1); 27 //bank.save01(1); 28 //bank.save02(1); 29 } 30 } 31 32 ================================= 33 34 package com.test.multiThread; 35 36 import java.util.ArrayList; 37 38 public class MultiThreadDemo { 39 public static void main(String[] args) throws InterruptedException { 40 Bank bank = new Bank(); 41 System.out.println(bank.getAccount()); 42 ArrayList<Thread> list = new ArrayList<>(); 43 for (int i = 0; i < 100000; i++){ 44 list.add(new Thread(new MyThread(bank))); 45 } 46 for (Thread thread: list){ 47 thread.start(); 48 } 49 for (Thread thread: list){ 50 thread.join(); 51 } 52 System.out.println(bank.getAccount()); 53 } 54 }
只要把多线程访问的资源的操作放到multiThreadAccess方法中,就能够保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。为了提高程序的执行效率,Java语言提供了synchronized块。
第二种 同步代码块
即synchronized关键字修饰的语句块。被synchronized修饰的语句块会自动被加上内置锁,从而实现同步。
同步是一种高开销的操作,因此应该尽量减少同步的内容,通常没有必要使用同步方法,使用同步代码块来同步关键代码即可。
可以把任意的代码块声明为synchronized,也可以制定上锁的对象,有非常高的灵活性。用法如下
1 package com.test.multiThread; 2 3 public class Bank { 4 private int account = 0; 5 6 public int getAccount(){ 7 return account; 8 } 9 // 同步代码块 10 public void save(int money){ 11 synchronized (this){ 12 this.account += money; 13 } 14 } 15 } 16 17 =============================== 18 19 package com.test.multiThread; 20 21 public class MyThread implements Runnable { 22 private Bank bank; 23 public MyThread(Bank bank){ 24 this.bank = bank; 25 } 26 @Override 27 public void run() { 28 bank.save(1); 29 } 30 } 31 32 33 =============================== 34 35 package com.test.multiThread; 36 37 import java.util.ArrayList; 38 39 public class MultiThreadDemo { 40 public static void main(String[] args) throws InterruptedException { 41 Bank bank = new Bank(); 42 System.out.println(bank.getAccount()); 43 ArrayList<Thread> list = new ArrayList<>(); 44 for (int i = 0; i < 100000; i++){ 45 list.add(new Thread(new MyThread(bank))); 46 } 47 for (Thread thread: list){ 48 thread.start(); 49 } 50 for (Thread thread: list){ 51 thread.join(); 52 } 53 System.out.println(bank.getAccount()); 54 } 55 }
当使用synchronized来修饰某个共享资源的时候,如果线程Thread01在执行synchronized代码,另外一个线程Thread02也要同时执行同一对象的统一synchronized代码时,线程Thread02将要等到线程Thread01执行成后才能继续执行。在这种情况下,可以使用wait()方法和notify()方法。
在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify()方法或者notifyAll()方法通知正在等待的而其他线程,notify()唤醒一个线程(等待队列中的第一个线程),并允许它去获得锁,而notifyAll()方法唤醒所有等待这个对象的线程,并允许它们去竞争获得锁。
第三种 使用特殊成员变量(volatile 成员变量)实现线程同步(前提是对成员变量的操作是原子操作)
volatile是一个类型修饰符,被设计用来修饰被不同线程访问和修饰的变量。当变量没有被volatile修饰时,线程读取数据时可能会从缓存中去读取,如果其他线程修改了该变量,则无法读取到修改后的数据。当变量被volatile修饰时,线程每次使用时都会直接到内存中提取,而不会利用缓存,从而保证了数据的同步。
volatile关键字主要目的是放置编译器对代码的优化,使得每次使用数据的时候都从内存里提取,而不是缓存,保证获得的数据是最新被修改的数据。但是volatile不能保证操作的原子性,一般不能替代synchronized代码块,除非对变量的操作是原子操作的情况下才可以使用volatile。
① volatile关键字为成员变量的访问提供了一种免锁机制,但要保证对成员变量的操作是原子操作的情况下才能使用
② volatile关键字相当于告诉虚拟机该成员变量可能会被其他线程修改
③ 每次使用被volatile修饰的成员变量都要从内存提取,重新计算,而不会使用寄存机器中的值
④ volatile不会提供任何原子操作,不能保证线程安全
⑤ volatile不能用来修饰final类型的变量
⑥ 使用volatile会降低程序的执行效率
Java中原子性保证:Java内存模型只保证了基本读取和复制是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock保证任一时刻只有一个线程执行该代码,那么自然就不存在原子性问题了,从而保证了原子性。
Java中可见性保证:synchronized和Lock、volatile三种,推荐使用synchronized方式,volatile有局限,适合某个特定场合。
第四种 使用Lock接口(java.util.concurrent.locks包)
JDK5新增了一个java.util.concurrent.locks包来支持同步。该包中提供了Lock接口以及它的一个实现类ReentrantLock(重入锁)
Lock接口也可以用来实现多线程的同步,其提供了如下方法来实现多线程的同步
1 public abstract void lock() // 以阻塞方式来获得锁,即如果获得了锁就立即返回,如果其他线程持有锁,当前线程等待,直到获取锁后返回。当前线程会一直处于阻塞状态,且会忽略interrupt()方法 2 public abstract boolean tryLock() // 以非阻塞的方式获得锁,即尝试性的去获取锁,如果获得锁就返回true,否则返回false 3 public abstract boolean tryLock(long time, TimeUnit unit) // 如果在给定时间内获得锁,返回true,否则返回false 4 public abstract void lockInterruptibly // 如果获得锁,则立即返回,如果没有获得锁,则当前线程会处于休眠状态,直到获得锁,或者当前线程被其他线程中断(会收到InterruptedException异常)。 5 public abstract void unlock // 释放锁
ReentrantLock类的构造方法
1 public ReentrantLock() // 创建一个ReentrantLock实例 2 public ReentrantLock(boolean fair) // 创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
使用Lock接口实现多线程同步的例子
1 package com.test.multiThread; 2 3 import java.util.concurrent.locks.Lock; 4 import java.util.concurrent.locks.ReentrantLock; 5 6 public class Bank { 7 private int account = 0; 8 private Lock lock = new ReentrantLock(); // 声明这个重入锁 9 10 public int getAccount(){ 11 return account; 12 } 13 public void save(int money){ 14 lock.lock(); // 以阻塞方式获得锁 15 try { 16 account += money; 17 } finally { 18 lock.unlock(); // 释放锁 19 } 20 } 21 } 22 23 ============================= 24 25 package com.test.multiThread; 26 27 public class MyThread implements Runnable { 28 private Bank bank; 29 public MyThread(Bank bank){ 30 this.bank = bank; 31 } 32 @Override 33 public void run() { 34 bank.save(1); 35 } 36 } 37 38 ============================= 39 40 package com.test.multiThread; 41 42 import java.util.ArrayList; 43 44 public class MultiThreadDemo { 45 public static void main(String[] args) throws InterruptedException { 46 Bank bank = new Bank(); 47 System.out.println(bank.getAccount()); 48 ArrayList<Thread> list = new ArrayList<>(); 49 for (int i = 0; i < 100000; i++){ 50 list.add(new Thread(new MyThread(bank))); 51 } 52 for (Thread thread: list){ 53 thread.start(); 54 } 55 for (Thread thread: list){ 56 thread.join(); 57 } 58 System.out.println(bank.getAccount()); 59 } 60 }
第五种 使用线程局部变量(thread-local)解决多线程对同一变量的访问冲突,而不能实现同步 (ThreadLocal类)
1 public class ThreadLocal<T> 2 extends Object
如果使用ThreadLocal来管理变量,则每一个使用该变量的线程都会获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。所以对于同线程对共享变量的操作互不影响。
public class ThreadLocal<T> extends Object 常用方法 public ThreadLocal() // 构造方法 public T get() // 返回次线程局部变量的当前线程副本中的值 public void set(T value) // 将次线程局部变量的当前线程副本中的值设置为value protected T initialValue() // 返回次线程局部变量的当前线程的初始值 public void remove() //
Thread-local与同步机制的比较:
1)两者都是为了解决多线程中相同变量的访问冲突问题
2)Thread-local采用“空间换时间”方法,同步机制采用“时间换空间”的方式
使用Thread-local的例子
1 package com.test.multiThread; 2 3 public class Bank { 4 private static ThreadLocal<Integer> account = ThreadLocal.withInitial(() -> 0); 5 public void save(int money){ 6 account.set(account.get() + money); 7 } 8 public int getAccount(){ 9 return account.get(); 10 } 11 } 12 13 ============================ 14 15 package com.test.multiThread; 16 17 public class MyThread implements Runnable { 18 private Bank bank; 19 public MyThread(Bank bank){ 20 this.bank = bank; 21 } 22 @Override 23 public void run() { 24 for (int i = 1; i < 10; i++){ 25 bank.save(i); 26 } 27 System.out.println("Thread-local中的值: " + bank.getAccount()); 28 } 29 } 30 31 ============================ 32 33 package com.test.multiThread; 34 35 import java.util.ArrayList; 36 37 public class MultiThreadDemo { 38 public static void main(String[] args) throws InterruptedException { 39 Bank bank = new Bank(); 40 System.out.println("原始值:" + bank.getAccount()); 41 ArrayList<Thread> list = new ArrayList<>(); 42 for (int i = 0; i < 10; i++){ 43 list.add(new Thread(new MyThread(bank))); 44 } 45 for (Thread thread: list){ 46 thread.start(); 47 } 48 for (Thread thread: list){ 49 thread.join(); 50 } 51 System.out.println("原始值:" + bank.getAccount()); 52 } 53 }
结果:改变的只是线程中变量的值,线程结束后Thread-local变量就销毁了
第六种 使用阻塞队列实现线程同步(java.util.concurrent包)
在JDK5提供的java.util.concurrent包中的 Class LinkedBlockingQueue<E> 可以实现线程的同步。
LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。其常用方法如下:
1 public LinkedBlockingQueue() //创建一个容量为Interger.MAX_VALUE的LinkedBlockingQueue 2 public int size() // 返回队列中的元素个数 3 public void put(E e) throws InterruptedException // 在队尾添加一个元素,如果队列满则阻塞 4 public E take() throws InterruptedException // 返回并移除对首元素,如果队列空则阻塞
使用阻塞队列实现生产者-消费者。总的来说生产者的速度和消费者的速度相同,但是因为阻塞队列的缘故,不需要控制阻塞,当阻塞对列满的时候,生产者线程就会被阻塞,直到不再满;反之亦然,当消费者线程多于生产者线程时,消费者速度大于生产者速度,当队列为空时,就会阻塞消费者线程,直到队列非空。
1 package com.test.multiThread; 2 3 import java.util.concurrent.BlockingQueue; 4 import java.util.concurrent.LinkedBlockingQueue; 5 6 public class WorkDesk { 7 private BlockingQueue<String> desk = new LinkedBlockingQueue<>(10); 8 public void washDish() throws InterruptedException{ 9 desk.put("盘子"); 10 } 11 public String useDish() throws InterruptedException{ 12 return desk.take(); 13 } 14 } 15 16 ================================= 17 18 package com.test.multiThread; 19 20 public class Producer implements Runnable { 21 private String producerName; 22 private WorkDesk workDesk; 23 24 public Producer(String producerName, WorkDesk workDesk){ 25 this.producerName = producerName; 26 this.workDesk = workDesk; 27 } 28 @Override 29 public void run() { 30 try { 31 while (true) { 32 workDesk.washDish(); 33 System.out.println(producerName + "洗好一个盘子"); 34 Thread.sleep(1000); 35 } 36 } catch (Exception e){ 37 e.printStackTrace(); 38 } 39 } 40 } 41 42 ================================= 43 44 package com.test.multiThread; 45 46 public class Consumer implements Runnable { 47 private String consumerName; 48 private WorkDesk workDesk; 49 50 public Consumer(String consumerName, WorkDesk workDesk){ 51 this.consumerName = consumerName; 52 this.workDesk = workDesk; 53 } 54 55 @Override 56 public void run() { 57 try { 58 while (true) { 59 workDesk.useDish(); 60 System.out.println(consumerName + "使用一个盘子"); 61 Thread.sleep(1000); 62 } 63 } catch (Exception e){ 64 e.printStackTrace(); 65 } 66 } 67 } 68 69 ================================= 70 71 package com.test.multiThread; 72 73 import java.util.concurrent.ExecutorService; 74 import java.util.concurrent.Executors; 75 76 public class TestBlockingQueue { 77 public static void main(String[] args){ 78 WorkDesk workDesk = new WorkDesk(); 79 80 ExecutorService service = Executors.newCachedThreadPool(); 81 Producer producer01 = new Producer("生产者-1-", workDesk); 82 Producer producer02 = new Producer("生产者-2-", workDesk); 83 84 Consumer consumer01 = new Consumer("消费者-1-", workDesk); 85 Consumer consumer02 = new Consumer("消费者-2-", workDesk); 86 87 service.submit(producer01); 88 service.submit(producer02); 89 service.submit(consumer01); 90 service.submit(consumer02); 91 } 92 }
第七种 使用原子变量实现线程同步(java.util.concurrent.atomic包)
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即这几步要么同时完成,要么都不完成。
在JDK5中提供的java.util.concurrent.atomic包中提供了创建原子类型变量的工具类,使用这些工具类能够简化线程同步。
1 package com.test.multiThread; 2 3 import java.util.concurrent.atomic.AtomicInteger; 4 5 public class Bank { 6 private AtomicInteger account = new AtomicInteger(0); // 创建具有给定初始值的新的AtomicInteger 7 8 public int getAccount(){ 9 return account.get(); // 获取当前值 10 } 11 12 public void save(int money){ 13 account.addAndGet(money); // 以原子方式将给定值与当前值相加 14 } 15 } 16 17 ================================ 18 19 package com.test.multiThread; 20 21 public class MyThread implements Runnable { 22 private Bank bank; 23 public MyThread(Bank bank){ 24 this.bank = bank; 25 } 26 @Override 27 public void run() { 28 bank.save(1); 29 } 30 } 31 32 ================================ 33 34 package com.test.multiThread; 35 36 import java.util.ArrayList; 37 38 public class MultiThreadDemo { 39 public static void main(String[] args) throws InterruptedException { 40 Bank bank = new Bank(); 41 System.out.println("原始值:" + bank.getAccount()); 42 ArrayList<Thread> list = new ArrayList<>(); 43 for (int i = 0; i < 100000; i++){ 44 list.add(new Thread(new MyThread(bank))); 45 } 46 for (Thread thread: list){ 47 thread.start(); 48 } 49 for (Thread thread: list){ 50 thread.join(); 51 } 52 System.out.println("线程执行完后:" + bank.getAccount()); 53 } 54 }