Java[学习总结]-多线程(五)之线程的同步

1.为什么要让线程同步

(1)当多个线程同时访问同一个数据的时,非常容易出现线程安全。“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得到的结果也就是“线程安全”的。
(2)如果多个线程共同访问1个对象中的实例变量则有可能出现“非线程安全”。
(3)当多个线程对共享一个变量的时候,会产生”非线程安全”问题。会导致在操作过程中会出现值被改变,值不同步的情况,进而影响程序的执行流程。例如在某些JVM中,i–(非原子操作)的操作分为如下三步:取得原值i值加载到寄存器,计算i-1,将结果i写回寄存器.

2.同步规则

如果你正在写一个变量,它可能接下来会被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。

3.同步方法Synchronized(重点)

3.1 概念定义
同步方法就是使用synchronized关键字来修饰某个方法。当多个线程调用这个方法的时,以排队的方式进行处理。当一个线程调用同步方法的时候,必须等到其他线程对同步方法调用结束后方能使用。同步方法不具有继承性。

3.2 使用
(1)同步方法就是使用synchronized关键字来修饰某个方法。当多个线程调用这个方法的时,以排队的方式进行处理。当一个线程调用同步方法的时候,必须等到其他线程对同步方法调用结束后方能使用。
(2)同步方法不具有继承性。
(3)在使用时,应该将需要使用到的数据成员都定义为private,并且只能通过方法来访问这些数据成员,用synchronized关键字来修饰该方法

     public static synchronized void f(){};
     public synchronized void g(){};
     //注意,返回类型和方法名一般是绑定在一起,static 和 synchronized关键字都应该返回类型之前。

3.3 解释
(1)关键字synchronized取的是锁都是对象锁,而不是代码或者是方法当作锁。当多个线程访问的是同一个对象的同步方法的时候是排队的,而当多个线程访问多个对象的同步方法的时候运行的顺序是异步的。
请一定要记住“共享”这两个字,只有共享资源的读写访问才需要同步化。
(2)当多个线程访问的是同一个对象的同步方法的时候是排队的。
A线程先持有object对象的锁,B线程可以以异步的方式调用object对象总的非synchronizaed类型的方法。
A线程先持有object对象的锁,B线程如果调用object的synchronizaed类型的方法则需要等待,也就是同步。

3.4 注意
(1)当A线程调用任何Object对象加入synchronized关键字的X方法时,A线程获得X方法锁(对象锁),所以其他线程必须等A线程执行完毕才能调用X方法,但B线程可以随意调用其他的非同步方法。
(2)如果B线程调用的也是加入synchronized关键字的非X方法时,必须等待A线程将X方法执行完,才能执行非X方法。

3.5 数据脏读
(1)为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。虽然在赋值时进行了同步,但是可能在取值的时候出现脏读(dirtyRead)的现象。发生脏读的情况是在读取实例变量时。
(2)出现脏读是应为getValue方法不是同步方法,解决方法可以定义为同步方法。

4.同步代码块

4.1 语法
为了解决进程安全问题,Java的多线程支持引入同步监视器。使用同步监视器的方法就是同步代码块。语法如下:

//线程开始执行同步代码块之前,必须先获得对同步监控器的锁定
synchronized (Obj){
//此处的代码就是同步代码块
}

通常使用可能被并发访问的共享资源充当同步监视器。

public class MyThread extends Thread{
    private static Object obj;
    public static void main(String[] args) {
        obj = new Object();
        Thread t1 = new Thread(new RunnableOne());
        t1.start();
            }
    @Override
    public void run() {
        super.run();
        test();
        synchronized (obj){
            for (int i =0;i<9;i++){
                System.out.println("MyThread线程开启");
            }
        }
    }
    public void test(){
        synchronized (obj){//在方法内定义同步代码块,同时注意该对象参数,同步代码块锁的就是该对象
            
        }
    }
}

4.2 注意
(1)任何时刻只能有一条线程可以获得对同步监控器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监控器的锁定。虽然程序允许使用任何对象作为同步监控器,通常使用可能被并发访问的共享资源充当同步监视器。
(2)半异步一半同步:在同步块外是异步,在同步块内是同步。当一个线程访问object的一个synchronized(this)同步代码时,其他线程对同一个object中的其他的synchronized(this)同步代码块的访问将被阻塞,说明synchronized使用的是“对象监视器synchronized(this)和synchronized方法监视器都是当前对象this,调用的效果都是按顺序执行,也就是同步的,阻塞的。
(3)Java还支持对“任意对象”作为”对象监视器”这个“任意对象”大多数是共享资源、实例变量以及方法的参数。sychronized(非this对象x)在多个线程持有”对象监视器”为同一个对象的前提下,同一时间只有一个线程可执行synchronized(x){ }同步代码中的代码,而如果不是同—个对象监视器,运行的结果就是异步调用。

(4)在sychronized(非this对象x)这种写法下,当其线程执行x对象中的synchronized同步方法时呈现(同步)效果,当其他线程执行x对象里面synchronized(this)代码块时呈现(同步)的效果。

4.3 释放同步监控器的锁定
线程会在如下情况下释放对同步监控器的锁定:
(1)当前线程的同步方法或代码块执行结束
(2)当前线程的同步方法或代码块中遇到break、return终止了该代码块、方法的继续执行
(3)当前线程的同步方法或代码块中出现未处理的Error或Exception
(4)当前线程的同步方法或代码块中执行了同步监控器对象的wait()方法

4.4 不释放同步监控器锁定的情况
(1)当前线程的同步方法或代码块调用
(2)Thread.sleepO,Thread.yield0方法来暂停当前线程执行
(3)当前线程的同步方法或代码块时,其他线程调用了该线程的suspend方法让该线程挂起

5.静态同步方法

5.1 概念理解
(1)关键字synchronized还可以应用到static静态方法上,如果这么写的话,那么就对当前的”*.java”文件对应的Class类进行持锁。而关键字synchronized关键字加到非static静态方法上是给对象上锁。
(2)当两个线程同时访问同一个类的静态同步方法和实例的非static静态方法时,两个方法是同步,这是因为Class锁可以对类的所有对象起作用。
(3)synchronized(class)代码块的作用实际上和synchronized static方法是一样的。

5.2 不用String作为锁对象
由于在JVM中具有String常量池缓存的功能,因此在大多数的情况下,synchronization代码块都不使用String作为锁对象,而改用其他的,比如new Object()实例化一个0bject对象。

6.Lock同步锁

6.1 概念
Java提供了另一种线程同步的机制:它通过显示定义同步锁对象来实现同步。由Lock对象充当同步锁。一般使用可重入锁(ReentrantL ock)。线程在访问共享资源之前先获取Lock对象,加锁,释放锁。
使用Lock对象来同步时候,锁定和释放出现在不同的作用范围中时,通常建议使用finally块来确保最终的释放。Lock锁是通过代码实现的,要保证锁定一定会被释放。

6.2 语法

Class A{
private final ReentrantLock lock= new ReentrantLock();
//需要保证线程安全的方法
public void m({
//加锁
lock.lock();
try{
finally{ 
lock.unlock();
}
}}

7.同步方法的比较

(1)同步方法和同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在同一个块结构中,而且当获取多个锁的时候,他们必须按照相反的顺序依次释放。
(2)Lock方法不仅可以使用与非结构快中,还可以试图中断锁和再次加锁的功能。被Lock加锁的代码中还可以嵌套调用。
(3)资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍。

8.可见性

(1)我们希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
(2)在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些调整。这种现象叫做”重排序”, 例如:有两个语句

int a= 1;//(执行时间100ms)
int b = 2;//(执行时间20ms)

int a= 1(执行时间100ms);int b = 2 (执行时间20ms);不管是谁前谁后,a和b的值不会变化。重排序的作用是优化编译器。但是在缺乏足够同步的多线程中,重排序是有可能导致线程安全问题的。

9.volatile关键字的作用

9.1 概念
Java语言提供了一种稍弱的同步机制,即volatile变量。当把变量声明为volatile类型后,编译器不会对它重排序。关键字volatile的作用是强制性从公共堆栈中(存放实例)中取得修饰的变量的值,而不是从线程的私有数据栈中取得变量的值。增加实例变量在多个线程之间的可见性。
在这里插入图片描述

9.2 非原子特性
关键字volatile提示线程每次都从共享内存中读取变量,而不是从线程的私有内存中读取。这样就保证了同步数据的可见性。但是如果修改变量中的数据,比如i++这种非原子操作,也是非线程安全的。
(1)取得原值i值加载到寄存器
(2)计算i的值
(3)将结果i写回寄存器
假如在第二步的时候,另外一个线程也修改了i的值出现了脏读的情况。需要用关键字synchronized来解决。

9.3 注意事项
(1)关键字volatile修饰的变量,JVM虚拟机只能保证从主内存加载到线程工作内存的值是最新的,可以解决变量读时的可见性问题。可是无法保证原子性,
(2)对于多个线程访问同一个实例变量还是需要加锁同步。假设对共享变量除了赋值之外并不完成其他操作,可以声明为volatile变量。否则需要考虑和synchronized关键字。

10.死锁

10.1 死锁影响
当两个线程相互等待对象释放同步监视器的时候就会发生死锁,Java虛拟机没有检测也没有采用措施处理死锁的情况。一旦出现死锁,整个程序既不会发生任何异常也不会给出任何提示,只是所有的进程都处于阻塞状态,无法继续。使用一种称为资源排序的简单技术可以轻易避免死锁。其核心思想就是各线程按照顺序依次对个对象进行加锁。

10.2 死锁原因
双方持有对方需要的锁,双方又再相互等待对方先释放锁,从而造成线程阻塞。
在这里插入图片描述10.3 编程策略
(1)避免多次锁定:尽量避免同一个线程对多个同步监听器进行锁定
(2)具有相同加锁顺序:如果多个线程需要对多个同步监听器进行锁定,则应该保证它们以相同的顺序请求加锁
(3)使用定时锁:程序调用Lock对象的尝试Lock()加锁时可指定time和unit参数,当超过指定时间后自动释放对Lock的锁定。
(4)死锁检测:这是一种依靠算法来实现的死锁预防机制,它主要针对那些不可能实现按序加锁也不能使用定时锁的场景。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值