多线程访问同一资源时,很容易出现线程安全问题。例如,当多个线程同时修改数据时,会导致某些线程修改数据丢失。这也是多线程访问冲突如何解决问题,为避免同时多线程访问公共资源,这个问题需要采用同步机制来解决。
以下是一些基本的同步机制:
第一种 同步方法
使用同步方法 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 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,上锁对象也可以制定,灵活性很高。用法如下
当使用当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不能用来修改final类型的变量
⑥ 使用volatile会降低程序的执行效率
Java中原子性保证:Java内存模型只保证基本读取和复制是原子性操作。如果要实现更大范围的原子性操作,可以通过synchronized和Lock保证任何时候只有一个线程执行代码,那么自然就没有原子性问题,从而保证原子性。
Java中的可见性保证:synchronized和Lock、volatile有三种,建议使用synchronized,volatile有限,适合特定场合。
第四种 使用Lock接口(java.util.concurrent.locks包)
Javava.util.concurrent.locks包支持同步。该包提供Lock接口及其实现Reentrantlock(重新入锁)
Lock接口也可以用来实现多线程同步,它提供了以下方法来实现多线程同步
1 public abstract void lock() // 通过阻塞获得锁,即如果获得锁,立即返回。如果其他线程持有锁,则等待当前线程,直到获得锁后返回。当前线程将始终处于阻塞状态,并将忽略interupt()方法
2 public abstract boolean tryLock() // 以非阻塞的方式获得锁,即尝试获得锁,如果获得锁,返回true,否则返回false
3 public abstract boolean tryLock(long time, TimeUnit unit) // 如果在给定的时间内获得锁,返回true,否则返回false
4 public abstract void lockInterruptibly // 如果获得锁,立即返回。如果没有锁,当前线程将处于休眠状态,直到获得锁,或当前线程将被其他线程中断(InteruptedException异常)。
5 public abstract void unlock // 释放锁
Reentrantlock类的结构方法
1 public ReentrantLock() // 创建Reentrantlock实例
2 public ReentrantLock(boolean fair) // 建立公平锁的结构方法,但不建议使用,因为它可以大大降低程序的运行效率
第五种 使用线程局部变量(thread-local)解决多线程对同一变量的访问冲突,无法实现同步 (Threadlocal类)
1 public class ThreadLocal
2 extends Object
如果使用如果Threadlocal管理变量,每个使用该变量的线程将获得该变量的副本,并且副本相互独立,因此每个线程可以在不影响其他线程的情况下随意修改自己的变量副本。因此,同一线程不会相互影响共享变量的操作。
public class ThreadLocal
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采用“空间换时间”采用同步机制的方法“时间换空间”的方式
第六种 使用阻塞队列实现线程同步(java.util.concurent包)
在Java.util.concurrent包中的concurent包 Class LinkedBlockingQueue 线程同步可以实现。
LinkedBlockingQueue是一个基于已连接节点的blocking queue。常用方法如下:
1 public LinkedBlockingQueue() ///创建一个容量,Interger.MAX_VALUELinkedBlockingQueue
2 public int size() // 返回队列中的元素数
3 public void put(E e) throws InterruptedException // 在队尾加一个元素,如果队列满了,就会被堵塞
4 public E take() throws InterruptedException // 返回并移除对首元素,如果队列空,则阻塞
使用阻塞队列实现生产者-消费者。一般来说,生产者的速度与消费者相同,但由于阻塞队列,不需要控制阻塞,当阻塞满时,生产者线程将被阻塞,直到不再满;相反,当消费者线程超过生产者线程时,消费者速度大于生产者速度,当队列空时,直到队列不空。
第七种 使用原子变量实现线程同步(java.util.concurrent.atomic包)
使用线程同步的根本原因是普通变量的操作不是原子。
原子操作是指将读取变量值、修改变量值、保存变量值作为一个整体来操作,即这些步骤要么同时完成,要么不完成。
在Java在JDK5中提供.util.concurrent.atomic包提供了创建原子类型变量的工具类,可以简化线程同步。
多线程访问冲突的问题不容低估,我们需要给予足够的重视,以避免这种情况。同时,要牢牢把握java多线程只有知识,才能在java编程更加规范,使其编程技术更加完善。