一、什么是线程同步?
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候, 我们就需要用到“线程同步” 。
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
二、什么是锁机制?
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized)。
当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:
- 1、一个线程持有锁会导致其它所有需要此锁的线程挂起
- 2、在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时引起性能问题
- 3、如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
三、如何实现同步?
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法: synchronized 方法和synchronized 块。
1、同步方法
public synchronized void method(int args) {}
synchronized方法控制对“成员变量|类变量”对象的访问:
每个对象对应一把锁, 每个synchronized方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
缺陷:若将一个大的方法声明为synchronized将会大大影响效率。
2、同步块
同步块: synchronized (obi) {}, obi称之为同步监视器。
obj可以是任何对 象,但是推荐使用共享资源作为同步监视器。
同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子。
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器未锁,锁定并访问
四、认识线程不安全
1、可能有重复数据:每个线程的工作空间与主内存在进行数据交互时,不一致(来不及更新数据,就完成了拷贝工作)。
2、不应该出现的负数:在模拟12306抢票中,对于最后一张票,即数据到临界值时,sleep等待完的线程拿不到“余票”。
实例一:12306抢票
我们之前有模拟过12306抢票,我们再来看一下:
public class UnsafeTest1 {
public static void main(String[] args) {
//一份资源
Web12306 web = new Web12306();
//多个代理
new Thread(web,"小明").start();
new Thread(web,"小军").start();
new Thread(web,"小强").start();
}
}
class Web12306 implements Runnable{
//票数
private int ticketNums = 20;
private boolean flag = true;
public void run(){
while(flag){
test();
}
}
public void test(){
if(ticketNums<0){
flag=false;
return;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
结果
抢到的票数不能为0,或者负数。
实例二:操作容器
对于数据又改又读取的情况,需要并发控制
public class UnsafeTest2 {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for(int i=0;i<10000;i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
}
结果
本该是10000的数据,现在丢失了。
五、实现线程安全
在并发是保证数据的准确性,效率尽可能高。
synchronized
- 1、同步方法
- 2、同步块
实例一:安全抢票模拟
public class SynTest1 {
public static void main(String[] args) {
//一份资源
SafeWeb12306 web = new SafeWeb12306();
//多个代理
new Thread(web,"小明").start();
new Thread(web,"小军").start();
new Thread(web,"小强").start();
}
}
class SafeWeb12306 implements Runnable{
//票数
private int ticketNums = 20;
private boolean flag = true;
public void run(){
while(flag){
test();
}
}
//线程安全,同步
//给资源上锁,即锁的是使用此方法的对象,让其他方法无法再访问,只能排队等待
public synchronized void test(){
if(ticketNums<=0){
flag=false;
return;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
给资源上锁,即锁的是使用此方法的对象,让其他方法无法再访问,只能排队等待。
结果
不再出现抢到重复的票,或者负数的票数了。
实例二:继续优化(double checking)
public class SynTest3 {
public static void main(String[] args) {
//一份资源
SynWeb12306 web = new SynWeb12306();
//多个代理
new Thread(web,"小明").start();
new Thread(web,"小军").start();
new Thread(web,"小强").start();
}
}
class SynWeb12306 implements Runnable{
//票数
private int ticketNums = 20;
private boolean flag = true;
public void run(){
while(flag){
//test1();
test2();
}
}
//double checking双重检测
public void test2(){
if(ticketNums<=0){ //考虑的是没有票的情况
flag=false;
return;
}
synchronized(this){
if(ticketNums<=0){ //考虑的是最后一张票的情况
flag=false;
return;
}
//模拟网络延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
//线程安全:同步方法
public synchronized void test1(){
if(ticketNums<=0){
flag=false;
return;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
}
}
分析
- 改成同步块:只锁其中一个成员变量无法实现,需要锁对象
- 锁的范围太小会锁不住,例如只锁if代码块的内容会失败
- 锁的范围太大,会出现性能低下的情况
- 所以尽可能锁定合理的范围(不是指代码,而是指数据的完整性)
六、死锁怎么办?
案例:
小红持有口红,1S后想要拿到镜子,而此时小强持有镜子,2S后想要拿到口红。两人彼此等待对方放下手中的镜子或口红,从而陷入僵局。
代码演示:
public class DeadLock {
public static void main(String[] args) {
Markup g1 = new Markup(1,"小红");
Markup g2 = new Markup(0,"小强");
g1.start();
g2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
//化妆
class Markup extends Thread{
static Lipstick lipstick=new Lipstick();
static Mirror mirror=new Mirror();
//选择
int choice;
//名字
String girl;
public Markup(int choice,String girl){
this.choice=choice;
this.girl = girl;
}
public void run(){
markup();
}
private void markup(){
if(choice==0){
synchronized(lipstick){
System.out.print(this.girl+"涂口红\r\n");
//1s后想拥有镜子的锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(mirror){
System.out.print(this.girl+"照镜子\r\n");
}
}
}else{
synchronized(mirror){
System.out.print(this.girl+"照镜子");
//2s后想拥有口红的锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lipstick){
System.out.print(this.girl+"涂口红");
}
}
}
}
}
结果
分析
- 过多的同步,可能造成线程相互不释放资源,从而相互等待。
- 一般发生于同步中持有多个对象的锁。
解决方法:不要在同一个代码块中,同时持有多个对象的锁。
改进
不要锁套锁
synchronized(mirror){
System.out.print(this.girl+"照镜子");
//2s后想拥有口红的锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//把锁分开
synchronized(lipstick){
System.out.print(this.girl+"涂口红");
}
改进后的结果
结果是我们想要的,我们解决了死锁的问题!!
七、处理并发协作
生产者消费者模式
- 假设仓库中只能存放件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
- 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件:
对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要 马上通知消费者消费。
对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
- synchronized可阻止并发更新同一 个共享资源,实现了同步;
- synchronized不能用来实现不同线 程之间的消息传递(通信)
解决方法一:管程法
- 用容器来实现,叫缓存区(消息队列)
- 优点:解耦,提高效率
解决方法二:红绿灯法
红绿灯法又叫标识法,通过设定标志位,提醒对象释放锁。
Java中的方法实现
方法名 | 作用 |
---|---|
final void wait() | 标识线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
final void wait(long timeout) | 指定等待的毫秒数 |
final void notifiy() | 唤醒一个处于等待状态的线程 |
final void notifiyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |