文章目录
线程同步的基本概念
- 线程同步:指的是一个线程发出某一个功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为了保持数据一致性,不能调用该功能。
- 数据胡乱原因:
1)资源共享(独享的资源则不会)
2)调度随机(意味着数据访问会出现竞争)
3)线程间缺乏必要的同步机制
同步机制(以买票为例)
了解同步机制之前先了解一下synchronized:
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁是依赖于底层的操作系统的Mutex Lock来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起来或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对较长的时间,时间成本相对比较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java6之后官方Java官方对从JVM层对synchronized较大优化,所以现在的synchroniezd锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、轻量级锁等技术来减少锁操作的开销。
第一种 同步代码块
使用方法:synchronized(同步监视器){
// 需要被同步的代码 :操作共享数据的代码,即为需要同步的代码。
共享数据:多个线程共同操作的数据
}
同步监视器:俗称锁,任何一个类的对象都可以充当锁,但是多个线程必须要共用同一把锁。
1.1、继承Thread类的线程
class Window extends Thread{
// 共享变量变成static
private static int ticket=100;
// private static Object object=new Object();
@Override
public void run() {
while(true) {
// synchronized (object){ // this 代表 w1,w2,w3
synchronized (Window.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class synchronizedThread {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
运行结果:
结论:如果使用其他对象的话要 static Object object =new Object() ,建议继承Thread的线程使用类的本身。
1.2、 实现Runnable的线程
class Window2 implements Runnable{
private Integer ticket=100;
@Override
public void run() {
while(true) {
synchronized (this) {// 此时的this就是唯一的代表 w
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class synchronizedRunnable {
public static void main(String[] args) {
Window2 w = new Window2();
new Thread(w,"窗口1").start();
new Thread(w,"窗口2").start();
new Thread(w,"窗口3").start();
}
}
运行结果:
结论:实现Runnable方法 同步监视器建议用this,其次共享数据我也没用static修饰,因为Window2类就实例化了一次。
1.3、实现Callable接口的线程
class Window3 implements Callable{
private Integer ticket=100;
private Integer num=0;
@Override
// 不仅可以抛出异常还可以返回值
public Integer call() throws Exception {
while(true){
synchronized (this) {// this就是w
if (ticket > 0) {
num+=ticket;
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
return num;
}
}
public class synchronizedCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Window3 w=new Window3();
FutureTask task = new FutureTask(w);
new Thread(task,"窗口1").start();
new Thread(new FutureTask(w),"窗口2").start();
new Thread(new FutureTask(w),"窗口3").start();
System.out.println("num的值为:"+task.get());
}
}
运行结果:
结论:Callable接口跟Runnable接口差异不大,就是Callable接口里定义的方法可以有返回值,可以声明抛出异常。
第二种 同步方法(还是取票为例)
使用方法:
如果操作共享的数据代码完整的声明在一个方法中,我们不妨把此方法声明为同步方法
2.1、继承Thread类的线程
class method extends Thread{
private static Integer ticket=100;
@Override
public void run() {
// 程序不会自动结束,我用了死循环
while(true){
buy();
}
}
// 因为是继承Thread,创建多个线程会创建method类的多个实例,必须声明为static
public synchronized static void buy(){ // 同步监视器还是类对象本身 method.class
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}
}
}
public class synchronizedThreadMethod {
public static void main(String[] args) {
method m1 = new method();
method m2 = new method();
method m3 = new method();
m1.setName("窗口1");
m2.setName("窗口2");
m3.setName("窗口3");
m1.start();
m2.start();
m3.start();
}
}
运行结果:
结论:同步方法还是有同步监视器,只不过是隐藏式的。
2.2 、实现Runnable接口的线程
class method2 implements Runnable{
private Integer ticket=100;
@Override
public void run() {
while(true){
buy();
}
}
public synchronized void buy(){ // 同步监视器为this ,而且方法也不是static的
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}
}
}
public class synchronizedRunnableMethod {
public static void main(String[] args) {
method2 m = new method2();
new Thread(m,"窗口1").start();
new Thread(m,"窗口2").start();
new Thread(m,"窗口3").start();
}
}
运行结果:
结论:实现方法的同步监视器为this,而且方法不是static的。
2.3、实现Callable接口的线程
class method3 implements Callable{
private Integer ticket=100;
public static Integer num=0;
@Override
public Integer call() throws Exception {
while(true){
buy();
}
}
public synchronized void buy(){
if (ticket > 0) {
num+=ticket;
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}
}
}
public class synchronizedCallableMethod {
public static void main(String[] args) {
method3 m = new method3();
new Thread(new FutureTask(m),"窗口1").start();
new Thread(new FutureTask(m),"窗口2").start();
new Thread(new FutureTask(m),"窗口3").start();
}
}
运行结果:
第三种 使用Lock(以ReentrantLock)
使用方法:
- 实例化 ReentrantLock
- try里面调用lock方法
- finally里面调用unlock方法
lock是一个接口,里面只定义了lock、trylock、unlock等方法,所以实现原理我们直接从ReentrentLock来看。ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer(简称AQS),线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争(ReentrantLock默认使用非公平锁,当我们调用ReentrantLock的lock方法的时候,实际上它调用的是非公平锁的lock(),这个方法先用CAS操作,去尝试抢占该锁。如果成功,就把当前线程设置在这个锁上,表示抢占成功,如果失败,就调用LockSupport.park将当前线程阻塞,将其加入CLH队列中,等待抢占),第二个阶段是基于CLH队列的竞争。
(然后进入CLH队列的抢占模式,当持有锁的那个线程调用unlock的时候,会将CLH队列的头结点的下一个节点线程唤醒,调用的是LockSupport.unpark()方法。)在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CLH队列的锁竞争中,依靠CAS操作来抢占锁,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。总体来说,ReentrantLock是一个比较轻量级的锁,而且使用面向对象的思想去实现了锁的功能,比原来的synchronized关键字更加好理解。
3.1、继承Thread类的线程
class Window extends Thread{
private static Integer ticket=100;
private static ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while(true){
try {
// lock中没有同步监视器,但是可以把lock看做同步监视器的作用
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
}finally {
// 手动解锁
lock.unlock();
}
}
}
}
public class lockThread {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
运行结果:
3.2、实现Runnable接口的线程
class Window2 implements Runnable{
private Integer ticket=100;
private ReentrantLock lock=new ReentrantLock();
@Override
public void run() {
while(true) {
try {
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
}finally {
lock.unlock();
}
}
}
}
public class lockRunnable {
public static void main(String[] args) {
Window2 w = new Window2();
new Thread(w,"窗口1").start();
new Thread(w,"窗口2").start();
new Thread(w,"窗口3").start();
}
}
运行结果:
3.3、实现Callable接口的线程
class Window3 implements Callable{
private Integer ticket=100;
private ReentrantLock lock=new ReentrantLock();
private Integer num=0;
@Override
public Object call() throws Exception {
while(true){
try {
lock.lock();
if (ticket > 0) {
num+=ticket;
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
}finally {
lock.unlock();
}
}
return num;
}
}
public class lockCallable {
public static void main(String[] args) {
Window3 w = new Window3();
FutureTask task = new FutureTask(w);
new Thread(task,"窗口1").start();
new Thread(new FutureTask(w),"窗口2").start();
new Thread(new FutureTask(w),"窗口3").start();
try {
System.out.println("num的值为:"+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
运行结果:
总结
- 区别1:Synchronized 是Java的一个关键字,而Lock是java.util.concurrent.Locks 包下的一个接口;
- 区别2:Synchronized 使用过后,会自动释放锁,而Lock需要手动上锁、手动释放锁。(在 finally 块中)
- 区别3:Lock提供了更多的实现方法,而且 可响应中断、可定时, 而synchronized 关键字不能响应中断;
- 区别4:synchronized关键字是非公平锁,即,不能保证等待锁的那些线程们的顺序,而Lock的子类ReentrantLock默认是非公平锁,但是可通过一个布尔参数的构造方法实例化出一个公平锁;
- 区别5:synchronized无法判断,是否已经获取到锁,而Lock通过tryLock()方法可以判断,是否已获取到锁;
- 区别6:Lock可以通过分别定义读写锁提高多个线程读操作的效率。
- 区别7:二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略(底层基于volatile关键字和CAS算法实现)
CAS算法:
1、CAS,即Compare And Swap,意思是:比较并替换。
2、CAS算法需要3个操作数:内存地址V,旧预期值A,将要更新的目标值B。
3、CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
4、CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
5、通常将 CAS算法 用于同步的方式是:从地址 V 读取值 A,执行多步计算来获得新值B,然后使用 CAS算法 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。