Java基础15:线程安全和同步

一、线程安全概述

1、定义:

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说,一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。(对全局变量和静态变量操作在多线程模型中会引发线程不安全)。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

2、总结:

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读写完,其他线程才可使用。不会出现数据不一致或者数据污染。 

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

各个线程可以访问进程中的公共变量和资源,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。


二、事务的原子性和原子性操作

1、事务是指对系统进行的一组操作,为了保证系统的完整性,事务需要具有ACID特性,具体如下:

(1)原子性(Atomic)

一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。

实现事务的原子性,要支持回滚操作,在某个操作失败后,回滚到事务执行之前的状态。 回滚实际上是一个比较高层抽象的概念,大多数DB在实现事务时,是在事务操作的数据快照上进行的(比如,MVCC),并不修改实际的数据,如果有错并不会提交,所以很自然的支持回滚; 而在其他支持简单事务的系统中,不会在快照上更新,而直接操作实际数据。可以先预演一遍所有要执行的操作,如果失败则这些操作不会被执行,通过这种方式很简单的实现了原子性。

(2)一致性(Consistency)

一致性是指事务使得系统从一个一致的状态转换到另一个一致状态。事务的一致性决定了一个系统设计和实现的复杂度。事务可以不同程度的一致性:

(3)隔离性(Isolation)

并发事务之间互相影响的程度,比如一个事务会不会读取到另一个未提交的事务修改的数据。在事务并发操作时,可能出现的问题比如脏读:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。

(4)持久性(Durability)

事务提交后,对系统的影响是永久的。

2、原子操作(atomic operation):是不需要synchronized,这是Java多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程)。

如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。

将整个操作视作一个不可分割的整体是原子性的核心特征


三、同步和异步

1.同步与异步

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态来通知调用者,然后通过回调函数处理这个调用。典型的异步编程模型比如Node.js

举个通俗的例子:

你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。

而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你,在这里老板通过“回电”这种方式来回调

2、阻塞和非阻塞

关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回;

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

综上所述:同步可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是阻塞的,只有接收到返回的值或消息后才往下执行其他的命令;异步可以理解为执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

①同步,就是实时处理(如打电话),比如服务器一接收客户端请求,马上响应,这样客户端可以在最短的时间内得到结果,但是如果多个客户端,或者一个客户端发出的请求很频繁,服务器无法同步处理,就会造成涌塞:

同步如打电话,通信双方不能断(我们是同时进行,同步),你一句我一句,这样的好处是,对方想表达的信息我马上能收到,但是,我在打着电话,我无法做别的事情。

②异步,就是分时处理(如收发短信),服务器接收到客户端请求后并不是立即处理,而是等待服务器比较空闲的时候加以处理,可以避免涌塞: 

异步如收发收短信,对比打电话,打电话我一定要在电话的旁边听着,保证双方都在线,而收发短信,对方不用保证此刻我一定在手机旁,同时,我也不用时刻留意手机有没有来短信。这样的话,我看着视频,然后系统通知来了短信,我就处理短信(也可以不处理),接着再看视频。

对于写程序,同步往往会阻塞,没有数据过来,我就等着;异步则不会阻塞,没数据来我干别的事,有数据来才去处理这些数据。

同步在一定程度上可以看做是单线程,这个线程请求一个方法后就待这个方法给他回复,否则他不往下执行;异步在一定程度上可以看做是多线程的(废话,一个线程怎么叫异步),请求一个方法后,就不管了,继续执行其他的方法。


四、对象锁

1、多线程开发中不可避免的要用到锁,一段被加锁的代码被一个线程执行之前,线程要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁,如果这个时候同步对象的锁被其他线程拿走了,这个线程就只能等了(线程阻塞在锁池等待队列中)。拿到锁后,它就开始执行同步代码,线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在同一时刻只有一个线程在执行。Java中常用的锁有synchronized和Lock两种。

2、每个锁对象(JLS(Java语言规范)中叫monitor)都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被notify唤醒后,才会进入到就绪队列,等待CPU的调度;反之,当一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒,这个涉及到线程间的通信。

举例:当第一个线程执行被synchronized修饰的输出方法时,获得同步锁,执行输出方法,恰好此时第二个线程也要执行此输出方法,但发现同步锁没有被释放,第二个线程就会进入就绪队列,等待锁被释放。

3、锁的特点:每个对象只有一把锁,不管是synchronized还是Lock,它们锁定的只能是某个具体对象,也就是说该对象必须是唯一的,才能被锁起,不被多个线程同时使用。

科普:在JVM运行时刻内存的分配中,其中有一个内存区域是JVM栈,每一个线程运行时都有一个私有的线程栈,线程栈保存了线程运行时候局部变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量,这样在堆中的对象的值就产生变化了。


五、Synchronized

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。

1、synchronized是Java中的一个关键字,是Java语言内置的特性,用来实现线程同步。使用synchronized修饰的方法或者代码块可以看成是一个原子操作。线程可以使用 synchronized 关键字来获取对象上的锁。

2、synchronized的特点:同步锁,当它锁定的方法或者代码块发生异常的时候,它会自动释放锁;但是如果被它锁定的资源被线程竞争激烈的时候,它的表现就没那么好了。

3、如果方法或代码块被synchronized修饰了,当一个线程获取了对象对应的锁,并执行该synchronized方法和代码块时,其他线程便只能一直等待获取锁的线程释放锁后才能执行,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,很影响程序的执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,那么当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁,这个是synchronized无法办到的。

使用synchronized在某些情况下还可能会造成死锁,使用Lock之后可以手动解决死锁问题。


六、Lock接口

JDK 1.5 通过 Lock 接口提供了显式(explicit)的锁机制,增强了灵活性以及对线程的协调。Lock 接口中定义了加锁lock()和解锁unlock()的方法,同时还提供了 new Condition() 方法来产生用于线程之间通信的 Condition 对象;

java.util.concurrent.lock包中常用的类和接口:
(1)Lock是一个接口:

public interface Lock 
{ 
   void lock();
   void lockInterruptibly() throws InterruptedException;
   boolean tryLock();
   boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
   void unlock();
   Condition newCondition();
}
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

 

Lock的实现类有读出锁ReadLock、写入锁WriteLock、重入锁ReenTrantLock: 

(2)ReentrantLock:

多线程开发中最常用的ReentrantLock可重入锁是实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

由于我们提到synchronized无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。所以JSR 166小组花时间为我们开发了java.util.concurrent.lock框架,当Lock锁定的方法或者代码块发生异常的时候,它不会自动释放锁;它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线程上。)


七、Synchronized和Lock的区别

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)synchronized是Java语言内置的关键字,而Lock是一个接口,可以实现同步访问,提供了比synchronized更多的功能。

2)采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

 

参考资料:

线程安全和线程同步Synchronized

synchronized 与和Lock使用


 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java架构何哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值