【Java 并发编程】【05】线程安全问题与线程同步

5. 线程安全问题与线程同步

多线程编程是有趣且复杂的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在运行过程中偶尔会出现问题,那也是由于我们的代码有问题导致的。当多个线程访问同一个数据时,非常容易出现线程安全问题。

5.1 线程安全问题

所谓线程安全问题,其实就是多线程在并发访问的时候,对共享内存中的共享对象属性进行修改所导致的数据冲突问题
Keyword:

  • 并发访问
  • 共享内存
  • 共享对象 (共享内存中的共享对象)
  • 属性的修改 (方法是在栈中,不会有并发问题)

JVM内存模型:
在这里插入图片描述
线程之间可以共享的内存有:
1.堆内存的数据(同一个对象的属性)
2.方法区中的数据(字符串,常量,类的静态属性)
共享数据尽量不要用静态的,因为静态生命周期太长了

解决方法:
1.同步代码块
2.同步方法

5.2 安全问题演示

关于线程安全问题,有一个经典的问题:卖票问题。卖票的基本流程很简单:看是否还有票,如果有就可以卖。

(1)资源类

public class TicketService {

    public int total = 10;

    public void saleTicket(){
        total--;
    }
    public boolean hasTicket(){
        return total > 0;
    }
    public int getTotal(){
        return total;
    }
}

(2)线程类

//买票窗口类
public class Saler extends Thread{

    private TicketService ts;
    
    //锁对象:Thread子类的静态属性=>所有Saler对象共享的同一个对象
    private static final Object obj = new Object();


    //一个窗口对应一个
    public Saler(){
        ts = new TicketService();
    }

    //多个窗口对应一个
    public Saler(TicketService ts) {
        this.ts = ts;
    }

    //todo 1.下面的代码会引发线程安全问题:
    public void run(){
            while(ts.hasTicket()){

                //这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    ts.saleTicket();
                    System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                }
            }
        System.out.println("没有票了");
    }
}

Demo1 同一个对象作为共享资源

public class TestSaler {
    public static void main(String[] args) {
        TicketService ts = new TicketService();

        //todo 注意s1和s2两个线程 共享同一个ts对象
        Saler s1 = new Saler(ts);
        Saler s2 = new Saler(ts);

        s1.start();
        s2.start();
    }
}
测试结果之一:
Thread-0买了一张票,余票:8
Thread-1买了一张票,余票:8
Thread-1买了一张票,余票:6
Thread-0买了一张票,余票:6
Thread-1买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-0买了一张票,余票:3
Thread-1买了一张票,余票:2
Thread-0买了一张票,余票:1
Thread-1买了一张票,余票:0
没有票了
Thread-0买了一张票,余票:-1
没有票了

这里就发生了线程安全问题,当多个线程多条语句(size()和remove())访问共享数据(这里TicketService的list存票的集合)时,就会发生线程安全问题。

解释1:“票卖超了”如何发生的,不是先判断是否有票,才买的吗?
在这里插入图片描述

run():
ts.saleTicket();
System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());
这两行代码不是原子性的;线程1执行了第一句后,由线程2执行,然后执行了第二句,又切换到线程1执行第二句

Demo2 不同对象的静态属性作为共享资源

public class TestSaler {
    public static void main(String[] args) {

        //s3和s4 两个线程对象中各自拥有两个TicketService对象,并且共享资源为total实例属性因此不算线程安全问题
        // 如果将TicketService类中的total属性改为静态的,仍然是线程安全问题的
        Saler s3 = new Saler();
        Saler s4 = new Saler();

        s3.start();
        s4.start();
    }
}

(1)s3和s4 两个线程对象中各自拥有两个TicketService对象,并且共享资源为total实例属性因此不算线程安全问题


Thread-0买了一张票,余票:9
Thread-1买了一张票,余票:9
Thread-1买了一张票,余票:8
Thread-0买了一张票,余票:8
Thread-0买了一张票,余票:7
Thread-1买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-1买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-1买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-1买了一张票,余票:4
Thread-1买了一张票,余票:3
Thread-0买了一张票,余票:3
Thread-0买了一张票,余票:2
Thread-1买了一张票,余票:2
Thread-1买了一张票,余票:1
Thread-0买了一张票,余票:1
Thread-1买了一张票,余票:0
没有票了
Thread-0买了一张票,余票:0
没有票了

(2)如果将TicketService类中的total属性改为静态的,仍然属于线程安全问题


public class TicketService {
	//改为静态的
    public static int total = 10;

    public void saleTicket(){
        total--;
    }
    public boolean hasTicket(){
        return total > 0;
    }
    public int getTotal(){
        return total;
    }
}
Thread-0买了一张票,余票:8
Thread-1买了一张票,余票:8
Thread-1买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-1买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-1买了一张票,余票:3
Thread-1买了一张票,余票:2
Thread-0买了一张票,余票:2
Thread-0买了一张票,余票:1
没有票了
Thread-1买了一张票,余票:0
没有票了

5.3 安全问题解决

如何解决线程安全问题呢?

1.同步监视器对象(锁对象)

解决思路:将线程中多条操作共享数据的语句封装成一个原子性操作,在线程执行这段原子性操作期间,其他线程不可以参与执行。
在这里插入图片描述

为了解决这个问题,Java的多线程支持引入同步监视器来解决这个问题。
使用同步监视器的方式有两种:同步代码块和同步方法。

任何共享资源的操作都需要放进同步代码块/方法中

2.同步代码块

  • 同步代码块的语法格式如下:
synchronized(同步监视器对象){
    //.....
}

上面代码的含义,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。

3.锁对象的选择

(1)任何对象都可以作为同步监视器对象。
(2)只要保证共享资源的这几个线程,只要锁的是同一个同步监视器对象即可
保证是同一个锁对象要么是堆内存中同一个对象;要么是方法区中同一个静态的对象;

3.1 共享资源对象作为同步监视器对象
package com.atguigu.part03;

public class Saler extends Thread{
	private TicketService ts;

	public Saler(TicketService ts) {
		super();
		this.ts = ts;
	}
	public void run(){
		while(true){
			synchronized (ts) {
				if(ts.hasTicket()){
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					try {
						String buy = ts.buy();
						System.out.println("购买票:" + buy);
					} catch (Exception e) {
						System.out.println(e.getMessage());
					}
				}else{
					System.out.println("没有票了");
					break;
				}
			}
		}
	}
}

要求在创建多个线程的时候,都需要传递同一个TicketService对象作为构造器参数;

3.2 选择this对象作为同步监视器对象
  • 如果线程是继承Thread类实现的,那么把同步监视器对象换成this,那么就没有起到作用,仍然会发生线程安全问题。因为两个线程的this对象是不同的。
  • 如果线程是实现Runnable接口实现的,那么如果两个线程共用同一个Runnable接口实现类对象作为target的话,就可以把同步监视器对象换成this。

(1)任务类

package com.atguigu.part03;

public class Window implements Runnable{
	private TicketService ts;

	public Window(TicketService ts) {
		super();
		this.ts = ts;
	}
	public void run(){
		while(true){
			synchronized (this) {
				if(ts.hasTicket()){
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					try {
						String buy = ts.buy();
						System.out.println("购买票:" + buy);
					} catch (Exception e) {
						System.out.println(e.getMessage());
					}
				}else{
					System.out.println("没有票了");
					break;
				}
			}
		}
	}
}

(2)

package com.atguigu.part03;

public class TestWindow {
	public static void main(String[] args) {
		TicketService ts = new TicketService();
		Window w = new Window(ts);
		Thread t1 = new Thread(w);
		Thread t2 = new Thread(w);
		
		t1.start();
		t2.start();
	}
}
3.3 静态对象作为同步监视器对象
上锁范围
//买票窗口类
public class Saler extends Thread{
    private TicketService ts;

    //锁对象:Thread子类的静态属性=>所有Saler对象共享的同一个对象
    private static final Object obj = new Object();

    public Saler(){
        ts = new TicketService();
    }

    public Saler(TicketService ts) {
        this.ts = ts;
    }

    //todo 2.上锁   锁的范围不能太大也不能太小,太大会导致别的线程不容易获得锁,太小无法保证线程安全
    // 下面这段代码所得范围太大,只能一个线程买票
    public void run(){
       synchronized (obj){
           while(ts.hasTicket()){
               //这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }

               try {
                   ts.saleTicket();
                   System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());
               } catch (Exception e) {
                   System.err.println(e.getMessage());
               }
           }
       }
        System.out.println("没有票了");
    }

执行结果:

package _15_多线程._06_线程安全问题;

public class TestSaler {
    public static void main(String[] args) {
        Saler s3 = new Saler();
        Saler s4 = new Saler();
        s3.start();
        s4.start();
    }
}

Thread-0买了一张票,余票:9
Thread-0买了一张票,余票:8
Thread-0买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-0买了一张票,余票:4
Thread-0买了一张票,余票:3
Thread-0买了一张票,余票:2
Thread-0买了一张票,余票:1
Thread-0买了一张票,余票:0
没有票了
没有票了

(2)缩小锁定范围:

package _15_多线程._06_线程安全问题;


//买票窗口类
public class Saler extends Thread{
    private TicketService ts;

    //锁对象:Thread子类的静态属性=>所有Saler对象共享的同一个对象
    private static final Object obj = new Object();


    //一个窗口对应一个
    public Saler(){
        ts = new TicketService();
    }

    //多个窗口对应一个
    public Saler(TicketService ts) {
        this.ts = ts;
    }

    //todo 3。缩小锁的范围:
    //       任何关于公共资源的操作都要放进同步代码块中
    //       但是循环又不能放进同步代码块中,因此切换判断条件,将判断条件放进循环内部!
    public void run(){

            //while(ts.hasTicket()){

            while(true){
               synchronized (obj){
                   if(ts.hasTicket()){
                       //这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果
                       try {
                           Thread.sleep(100);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }

                       try {
                           ts.saleTicket();
                           System.out.println(getName() + "买了一张票,余票:" + ts.getTotal());
                       } catch (Exception e) {
                           System.err.println(e.getMessage());
                       }
                   }else break;
               }
            }

        System.out.println("没有票了");
    }
}
package _15_多线程._06_线程安全问题;

public class TestSaler {
    public static void main(String[] args) {

        Saler s3 = new Saler();
        Saler s4 = new Saler();
        s3.start();
        s4.start();
    }
}

Thread-0买了一张票,余票:9
Thread-0买了一张票,余票:8
Thread-0买了一张票,余票:7
Thread-0买了一张票,余票:6
Thread-0买了一张票,余票:5
Thread-1买了一张票,余票:4
Thread-1买了一张票,余票:3
Thread-1买了一张票,余票:2
Thread-0买了一张票,余票:1
Thread-0买了一张票,余票:0
没有票了
没有票了

4. 同步方法

概念 Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法;

同步方法用于将对共享数据的操作封装到同步方法中,一般由共享类来提供,比如集合中的Vector和HashTable

同步方法的特点

  • 对于共享对象的synchronized方法 而言,同一时刻只能有一个线程访问
  • 如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法, 其它线程不能同时访问这个对象中任何一个synchronized方法.

这时,不同的对象实例的 synchronized方法是不相干扰的。
也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;

(1) 同步方法的锁对象

对于同步方法而言,无须显式指定同步监视器,也无法指定同步监视器。

  • 静态方法的同步监视器对象是提供此方法的类的Class对象
  • 非静态方法的同步监视器对象是提供此方法的this对象 (A.test() A是方法的提供者对象)

如果要在Thread类中编写同步方法封装对共享资源的操作,必须是静态同步方法;
如果在Runnable实现类中编写同步方法,可以是非静态的;

(1) 案例1 非静态同步方法

package _15_多线程._06_线程安全问题3;

public class Test {
    public static void main(String[] args) {

        Ticket t1 = new Ticket("t1");
        Ticket t2 = new Ticket("t2");
        Ticket t3 = new Ticket("t3");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Ticket extends Thread {


    public static int total = 10;

    public Ticket(String name){
        super(name);
    }


    //todo 同步方法,非静态的同步方法的锁对象为this
    // 当前为三个线程对象,因此锁对象不是同一个
    // 因此无法解决线程安全问题
    // 将此方法改为静态的即可
    private synchronized void saleOneTicket(){
        if(total > 0){
            System.out.println(getName()+"买了一张票");
            total--;
            System.out.println("余票"+total);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

   public void run(){
        while(total>0){
            saleOneTicket();
        }
   }
}

执行结果:

t1买了一张票
余票9
t2买了一张票
余票8
t3买了一张票
余票7
t1买了一张票
余票6
t3买了一张票
余票5
t2买了一张票
余票4
t2买了一张票
余票3
t1买了一张票
余票2
t3买了一张票
余票1
t1买了一张票
余票0
t2买了一张票
t3买了一张票
余票-2
余票-1

分析:由于是Thread类提供的非静态同步方法,因此锁对象为Thread对象,main方法中创建了三个Thread对象,因此锁对象根本就不是同一个,就无法保证线程安全;

(2)案例2 用静态同步方法解决线程安全问题

package _15_多线程._06_线程安全问题3;

public class Test2 {
    public static void main(String[] args) {

        Ticket1 t1 = new Ticket1("t1");
        Ticket1 t2 = new Ticket1("t2");
        Ticket1 t3 = new Ticket1("t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Ticket1 extends Thread {


    public static int total = 10;

    public Ticket1(String name){
        super(name);
    }


    //todo 改进:改成静态的方法即可
    // 同步方法就要求 共享资源必须为静态的
    private static synchronized void saleOneTicket(){
        if(total > 0){
            System.out.println(Thread.currentThread().getName()+"买了一张票");
            total--;
            System.out.println("余票"+total);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

   public void run(){
        while(total>0){
            saleOneTicket();
        }
   }
}

执行结果:

t1买了一张票
余票9
t3买了一张票
余票8
t3买了一张票
余票7
t3买了一张票
余票6
t3买了一张票
余票5
t3买了一张票
余票4
t3买了一张票
余票3
t2买了一张票
余票2
t3买了一张票
余票1
t3买了一张票
余票0

不要对线程安全类的所有方法都加同步,只对那些会影响竞争资源(即共享资源)的方法进行同步即可。而且也要注意同步方法的默认同步的监视器对象对于多个线程来说是否是同一个。

5. 线程安全的集合

Java提供了很多线程安全的集合;这些集合提供了很多同步方法即有synchronized修饰,以此保证操作集合时线程安全。

6. 释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
1、释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程的同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
  • 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起并释放锁

2、不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
    sleep()和yield()只释放CPU,不释放锁
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。
  • 应尽量避免使用suspend()和resume()这样的过时来控制线程
疑问
package com.atguigu.part03;

import java.util.ArrayList;

public class TicketService {
	private ArrayList<String> list;
	public TicketService(){
		list = new ArrayList<String>();
		list.add("01车01A");
		list.add("01车01B");
		list.add("01车01C");
		list.add("01车01D");
		list.add("01车01E");
		list.add("01车02A");
		list.add("01车02B");
		list.add("01车02C");
		list.add("01车02D");
		list.add("01车02E");
	}
	public synchronized boolean hasTicket(){
		return list.size()>0;
	}
	
	public synchronized String buy(){
		try {
			return list.remove(0);
		} catch (IndexOutOfBoundsException e) {
			throw new RuntimeException("票卖超了");
		}
	}
}
package com.atguigu.part03;

public class Saler extends Thread{
	private TicketService ts;

	public Saler(TicketService ts) {
		super();
		this.ts = ts;
	}
	public void run(){
		while(ts.hasTicket()){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			try {
				String buy = ts.buy();
				System.out.println("购买票:" + buy);
			} catch (Exception e) {
				System.err.println(e.getMessage());
			}
		}
		System.out.println("没有票了");
	}
}

package com.atguigu.part03;

public class TestSaler {
	public static void main(String[] args) {
		TicketService ts = new TicketService();
		Saler s1 = new Saler(ts);
		Saler s2 = new Saler(ts);
		
		s1.start();
		s2.start();
	}
}

上面run()中调用了两个同步方法,但是同步方法作用域不是就一段么,为啥能保证线程同步?

7. 死锁问题

不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

package com.atguigu.part03;

public class TestDeadLock {
	public static void main(String[] args) {
		Object g = new Object();
		Object m = new Object();
		
Owner s = new Owner(g,m);
		Customer c = new Customer(g,m);
		new Thread(s).start();
		new Thread(c).start();
	}
}
class Owner implements Runnable{
	private Object goods;
	private Object money;

	public Owner(Object goods, Object money) {
		super();
		this.goods = goods;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (goods) {
			System.out.println("先给钱");
			synchronized (money) {
				System.out.println("发货");
			}
		}
	}
}
class Customer implements Runnable{
	private Object goods;
	private Object money;

	public Customer(Object goods, Object money) {
		super();
		this.goods = goods;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (money) {
			System.out.println("先发货");
			synchronized (goods) {
				System.out.println("再给钱");
			}
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值