Java 里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性原子性。
Java 实现线程同步有如下几种方式
以下便一一讲解以下
synchronized 又称隐式锁(不需要加锁、解锁的操作)
synchronized 有两种使用方式:修饰方法、修饰代码块,特点如下:
// 加锁方法
public synchronized void synMethod){
// 方法体
}
// 加锁代码块
public void synMethod(){
synchronized(Object){ // Object 指锁对象
// 方法体
}
}
同步方法和同步代码块的区别
代码演示:
class SynObj{
public synchronized void showA(){
System.out.println("showA");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void showB(){
synchronized (this) {
System.out.println("showB");
}
}
public void showC(){
String s="1";
synchronized (s) {
System.out.println("showC");
}
}
}
public class Test {
public static void main(String[] args) {
final SynObj sy=new SynObj();
new Thread(new Runnable() {
public void run() {
sy.showA();
}
}).start();
new Thread(new Runnable() {
public void run() {
sy.showB();
}
}).start();
new Thread(new Runnable() {
public void run() {
sy.showC();
}
}).start();
}
}
执行以上代码,输出如下
showA
showC
showB
showB 是延迟3秒才打印的,这是因为同步方法showA 使用的是类锁。
Lock 是一个接口提供了无条件、可轮询、定时、可中断的锁获取操作,加锁和解锁都是显式的
包路径是:java.util.concurrent.locks.Lock
主要核心方法:lock()、tryLock()、unlock() 、lockInterruptibly ()
其实现类有:ReenTrantLock 、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WritedLock
1. Lock核心方法
lock()
是一个阻塞方法,调用后一直阻塞直到获得锁。阻塞过程中不会接受中断信号,忽视interrupt(), 拿不到锁就 一直阻塞。即:拿不到lock就不罢休,不然线程就一直block。 比较无赖的做法。
tryLock
马上返回,拿到lock就返回true,不然返回false。 比较潇洒的做法。带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。
lockInterruptibly
调用后如果没有获取到锁会一直阻塞,阻塞过程中会接受中断信号,即 线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。
2. Lock 主要方法
3. ReentrantLock
ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。ReentrantLock 类实现了 Lock ,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。
可重入的意思是:ReentrantLock锁可以被单个线程多次获取。
ReentrantLock分为“公平锁”和“非公平锁”
ReentrantLock 实现
public class MyService {
private Lock lock = new ReentrantLock();
//Lock lock=new ReentrantLock(true);//公平锁
//Lock lock=new ReentrantLock(false);//非公平锁
private Condition condition=lock.newCondition();//创建 Condition
public void testMethod() {
try {
lock.lock();//lock 加锁
//1:wait 方法等待:
//System.out.println("开始 wait");
condition.await();
//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
//:2:signal 方法唤醒
condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
4. ReentrantReadWriteLock
为了提高性能,Java 提供了读写锁ReentrantReadWriteLock,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁具有公平选择性、可重入性,锁降级的特点
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
总之,读的时候上读锁,写的时候上写锁!
Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 体 的 实 现ReentrantReadWriteLock。
5. synchronized 和 ReentrantLock 的区别
两者的共同点:
两者的不同点:
Condition
// 实例化一个ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
// 为线程A注册一个Condition
public Condition conditionA = lock.newCondition();
// 为线程B注册一个Condition
public Condition conditionB = lock.newCondition();
6. Condition 类和 Object 类锁方法区别
7. tryLock 和 lock 和 lockInterruptibly 的区别
volatile 变量具备两种特性:变量可见性、禁止重排序
即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。比 sychronized 更轻量级的同步锁
并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。
java多线程中的原子性、可见性、有序性
1. 变量可见性
其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
2. 禁止重排序
volatile 禁止了指令重排。所以具备 有序性,但不能保证原子性,所以不适用于高并发环境做安全机制
3. 为什么不能保证原子性
举例说明:线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
4. 适用场景
值得说明的是对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(booleanflag = true)。
该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。
5. volatile与synchronize的区别
很多地方 ThreadLocal 叫做线程本地变量,也有些地方叫做线程本地存储;ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
使用场景
最常见的 ThreadLocal 使用场景为 用来解决 数据库连接、Session 管理等。
示例:
// JDBC 获取连接
private static ThreadLocal threadLocal = new ThreadLocal();
public static Connection getConnection() {
Connection conn = threadLocal.get();
if (conn == null) {
try {
conn = DriverManager.getConnection(
properties.getProperty("jdbc.url"),
properties.getProperty("jdbc.username"),
properties.getProperty("jdbc.password")
);
threadLocal.set(conn);
} catch (Exception e) {
throw new RuntimeException("建立数据库连接异常", e);
}
}
return conn;
}
1. 原子操作类型
在Java并发比编程中,要想保证一些操作不被其他线程干扰,就需要保证原子性,在 java.util.concurrent.atomic 的包下JDK中提供了16(jdk8增加了4个) 个原子操作类来帮助我们进行开发。可以在高并发环境下的高效程序处理,其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,由于一般 CPU 切换时间比 CPU 指令集操作更加长, 所以 J.U.C 在性能上有了很大的提升。
原子类如下:
我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是 ReentantLock 的好几倍。
2. ConcurrentHashMap 并发
整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁,线程安全(Segment 继承 ReentrantLock 加锁)
如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行 put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行
具体可以看一下 [Java-集合篇(5)集合之Map ](Java-集合篇(5)集合之Map 章节.md)章节
3. CAS(比较并交换-乐观锁机制-锁自旋)
CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
三种CAS方法
Atomic包里的类基本都是使用Unsafe下的CAS方法实现,Unsafe只提供了三种CAS方法: compareAndSwapObject、compareAndSwapInt 和 compateAndSwapLong,其他类型都是转成这三种类型再使用对应的方法去原子更新的。
4. AQS(抽象的队列同步器)
AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它
范例演示:
public class AtomicIntegerTest {
private static final int THREADS_CONUT = 20;
public static AtomicInteger count = new AtomicInteger(0);
public static void increase() {
count.incrementAndGet();
}
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_CONUT];
for (int i = 0; i < THREADS_CONUT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(count);
}
}
5. ABA问题
CAS 会导致“ABA 问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题
信号量Semaphore是java.util.concurrent包下一个常用的同步工具类;Semaphore 基于计数的信号量它可以设定一个阈值,基于此AQS的共享模式,可以控制同时访问的线程个数,通过acquire() 获取一个许可,如果没有(超过阈值),线程申请许可信号将会被阻塞,而 release() 释放一个许可或者线程被中断
使用场景:
我们经常用信号量来管理可重复使用的资源,比如数据库连接、线程等,因为这些资源都有着可预估的上限
那么为什么Semaphore没有单独同重设信号量数量的方法呢?直接把AQS的setState方法暴露出来不就行的吗?
因为setState操作如果发生在在某些使用该Semaphore的线程还没有走完整个信号量的获取和释放的流程时,将会直接导致state值的不准确。
范例:
操场上有5个跑道,一个跑道一次只能有一个学生在上面跑步,一旦所有跑道在使用,那么后面的学生就需要等待,直到有一个学生不跑了。
//操场
public class Playground {
//跑道
static class Track {
private int num;
public Track(int num) {
this.num = num;
}
@Override
public String toString() {
return "Track{num=" + num +'}';
}
}
private Track[] tracks = {new Track(1), new Track(2), new Track(3), new Track(4), new Track(5)};
private volatile boolean[] used = new boolean[5];
private Semaphore semaphore = new Semaphore(5, true);
//获取一个跑道
public Track getTrack() throws InterruptedException {
semaphore.acquire(1);
return getNextAvailableTrack();
}
//遍历,找到一个没人用的跑道
private Track getNextAvailableTrack() {
for (int i = 0; i < used.length; i++) {
if (!used[i]) {
used[i] = true;
return tracks[i];
}
}
return null;
}
//释放跑道
public void releaseTrack(Track track) {
if (makeAsUsed(track)){
semaphore.release(1);
}
}
//确认跑道使用情况
private boolean makeAsUsed(Track track) {
for (int i = 0; i < used.length; i++) {
if (tracks[i] == track) {
if (used[i]) {
used[i] = false;
return true;
} else {
return false;
}
}
}
return false;
}
}
测试类
public class SemaphoreDemo {
static class Student implements Runnable {
private int num;
private Playground playground;
public Student(int num, Playground playground) {
this.num = num;
this.playground = playground;
}
@Override
public void run() {
try {
//获取跑道
Playground.Track track = playground.getTrack();
if (track != null) {
System.out.println("学生" + num + "在" + track.toString() + "上跑步");
TimeUnit.SECONDS.sleep(2);
System.out.println("学生" + num + "释放" + track.toString());
//释放跑道
playground.releaseTrack(track);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Executor executor = Executors.newCachedThreadPool();
Playground playground = new Playground();
for (int i = 0; i < 100; i++) {
executor.execute(new Student(i+1,playground));
}
}
}
定义:两个或者多个线程互相持有对方所需要的资源,导致这些线程处于无限期等待状态,无法继续执行
例如:线程1锁住了A资源并等待读取B资源,而线程2锁住了B资源并等待A资源,这样两个线程就发生了死锁现象。
死锁发生的条件:
如何避免线程死锁:只要破坏产生死锁的四个条件中的其中一个就可以了。
死锁范例
private static String A = "A";
private static String B = "B";
public static void main(String[] argc) {
new Thread(()-> {
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
}
}
}).start();
new Thread(()->{
synchronized (B) {
synchronized (A) {
}
}
}).start();;
}