线程的同步

线程的同步:

1、线程安全问题:当多个线程对具有共享变量的多个语句执行写操作(不能只看成执行同一个run方法,也可能在不同的run方法也有共享变量)时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程就开始执行。导致共享数据(共同操作的变量)的错误。

3、解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不能执行。对共享变量进行写入时,结果要正确,必须保证是原子操作原子操作是指不能被中断的一个或一系列操作

阻塞式synchronizedLOCK锁,其他线程会处于BLOCKED状态。

非阻塞式:原子变量

4、具体方法:

①synchronized同步代码块

格式:synchronized(锁){

需要被同步的代码

}

 

任何一个引用类型的对象都可以充当锁,但是也要把对象和锁的概念区分,一个对象同时可以作为对象和锁。一个线程执行过程中“锁”不能由其他线程使用,直到执行完毕把“锁”归还。但是操作共享数据的多个线程(不管是执行同一个run方法的多个线程还是执行不同run方法的多个线程)只能用一把锁,也就是只能用同一个对象。

①可以用this当锁,前提是确定this类只会有一个对象

Java还支持对“任意对象”作为对象监视器来实现同步的功能。这种任意对象大多是该方法所属类中的实例变量,不然抛开这个类去使用别的对象作为对象监视器,意义不大,前提也是该类只会创建一个对象;如果不能保证,则应该将该实例变量设为static

③不能保证该类只会创建一个对象时,用“该类名.class”表示锁。因为类本身是Class类的对象。

 

▲t1线程在执行过程中如果有其他CPU的调度让t2线程执行,但是运行到synchronized时,会会从RUNNABLE进入到BLOCKED状态。

    这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,同一个CPU把时间片分给了t2线程,如果也运行到了synchronized同一个锁,这时其他线程还会进入阻塞状态进不来,从RUNNABLE进入到BLOCKED状态,只有下次轮到 t1 自己再次获得时间片时才能开门进入

 

synchronized代码块要把共享数据全都包裹但不能包含过多其他代码。如果把while(true)包含,则会让先执行run的线程一一直循环,直到把ticket消耗完才出来。

 

 

②synchronized同步方法

如果操作共享数据的代码被写在一个方法中,我们不妨将整个方法加synchronized

格式:在由需要被同步代码组成的方法前加synchronized

▲同样有“锁”但是不用显式表示出来。

   不加static同步方法中的锁指“this”,也就是该类对象;

   加static同步方法中的锁指类.class。

 

▲同样含有synchronized方法的线程也会发生上下文切换,切换到没有锁的线程就罢了,该线程会正常运行,但一旦切换到执行到有同样锁的synchronized的线程,这个线程就会从RUNNABLE进入到BLOCKED状态。

 

同样要注意改同步方法也要包裹所有共享数据但不能包含过多代码,原因同上。

 

③Lock实现类锁(主要是ReentrantLock)

以Reentrantlock为例

相比synchronized的主要特点是:

可以对阻塞状态中断:

lock.lockInterruptibly()方法可以让在有线程竞争的时候,没有竞争到锁的线程在BLOCKED状态时可让别的线程调用interrupt方法人为地中断这种状态,可以用来解决死锁问题。synchronized不行,没有竞争到锁时只能阻塞等待。

 

注意这里的可中断指的是没有获取到锁进入到阻塞状态后的中断,而如果获取到锁,进入到代码块内,synchronized也是可以被interrupt打断的。

 

可以非阻塞加锁:

lock.tryLock方法是让线程尝试竞争锁,如果不成功不会像其他方法一样进入阻塞等待而是返回false,无论如何都会返回。lock.tryLock(Long)方法时让线程尝试竞争锁,如果竞争不到锁则在等待指定时间内保持运行RUNNABLE并同时竞争锁,如果在等待时间内获取不到锁,返回false,可以让我们决定接下来如何执行,最后返回boolean,第二种方法会在等待时间内被打断。两种方法通常都搭配if使用。

可以设置为公平锁:

以设置获取锁的顺序为先来后到,先进入阻塞队列的先获得锁

支持多个条件变量:

这里指ConditionObject,类似synchronized中的Waitset休息室,condition.await()在线程中调用这个方法表明让线程进入这个休息室,可以创建不同的休息室让不同线程等待。其意义就在于能精确唤醒某个线程。

共同特点是:

可重入:

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住 。

 

格式:class A{

 ReentrantLock reentrantLock = new ReenTrantLock(Boolean fair);

//fair=true,则是公平锁,加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得一个线程。若是非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

 

 

public void 方法名(){

reentrantLock.lock();

try{

//执行代码;

}

finally{

lock.unlock(); 

}

}

}

▲ReentrantLock和synchronized都是可重入锁

▲不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会    导致锁无故释放

▲Lock实现类锁和synchronized的异同:

   1、Lock实现类锁是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放

   2、Lock实现类锁只有代码块锁,synchronized有代码块锁和方法锁。

   3、使用Lock实现类锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

▲new ReenTrantLock(形参)构造器中可以选择boolean形参,true为线程公平。

▲在lock方法中,Reentrantlock对象就相当于“锁”,也不用显式的表示出来。

   在实现Runnable中,不用考虑static;

   在继承Thread中,要注意Reentrantlock一般要static

 

 

ReadWriteLock实现类锁(主要是ReentrantReadWriteLock)

    使用ReentrantLock锁的这种保护有时候会有点过头。因为我们发现,任何时刻,只允许一个线程修改,但是,get()方法只读取数据,不修改数据,它实际上允许多个线程同时调用。

   

当前线程获取读锁后,自身线程可以获取读锁不能获取写锁,其他线程可以获取读锁不能获取写锁,尝试获取锁的线程会进入阻塞队列,直到读锁释放

当前线程获取写锁后,自身线程可以获取读锁和写锁,其他线程不可以获取读锁和写锁。尝试获取锁的线程会阻塞,直到写锁释放。

 

格式:class A{

ReadWriteLock rwlock = new ReentrantReadWriteLock();
Lock rlock = rwlock.readLock();
Lock wlock = rwlock.writeLock();

 

public void 方法名(){

try{

rlock.lock();

//保证是只读取数据的代码,允许多个线程同时读取;

}

finally{

rlock.unlock(); 

}

}

public void 方法名(){

try{

wlock.lock();

//执行代码;

}

finally{

wlock.unlock(); 

}

}

}

⑤StampedLock锁(乐观锁)

和ReadWriteLock相比,写入的加锁是完全一样的,StampedLock把读锁细分为乐观读悲观读,能进一步提升并发效率。

 

 

 

5、共享变量的线程安全(建议看PDF)

这里的共享变量分为局部变量,静态变量,实例变量

实例变量和静态变量的线程安全(实例变量和静态变量在堆和方法区中,所有线程都可以访问到,容易出现安全问题):

如果变量只在一个线程中使用过或者是线程安全类,没有共享,那么线程对该变量操作是安全的

如果变量在线程间都有过操作

如果只有读操作,则线程安全

如果有读写操作,需要考虑线程安全问题

局部变量的线程安全(局部变量在栈中,而每个线程都有一个自己的栈,不容易出现安全问题):

局部变量被初始化为基本数据类型是安全的

局部变量是引用类型或者是对象引用则未必是安全的

如果局部变量引用的对象是和当前线程绑定的,即当前线程运行到该方法时新建一个引用对象,另外一个线程运行到该方法时又新建了另一个引用对象,没有共享,那么是线程安全的

如果局部变量引用的对象共享给了另外的线程(比如下面的例子,在子类中重写方法里创建新线程,由于三个方法的list是相同的list,所以对于主线程和新线程来说共享了list,则list局部变量也是不安全的),那么要考虑线程安全问题

▲对于局部变量所在方法来说,设置为private或final可以避免被子类重写,以免创建新线程而导致局部变量出现线程安全问题。

 

6、常见的线程安全类:

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的,因为有些方法加了锁或者类本身是不可变类(String,Integer),方法内的共享变量不会受到影响。

但注意它们多个方法的组合不是线程安全的 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值