Java多线程(二)线程同步

本文目录
线程同步简介
  需求
  原子操作
  synchronized简介
  JVM原子操作
  总结
synchronized使用详解
  加锁对象
  读取方法
  Thread-safe
  总结
死锁
  死锁现象
  死锁形成条件
  避免死锁
wait/notify
  生产者-消费者

线程同步简介

需求

多个线程同时运行,线程调度由操作系统决定,程序本身无法决定.

当多个线程同时读写同一个共享变量时,就会出现变量的值不准确的现象。

class Counter {
  	public static int count = 0;
}
class Addthread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	Counter.count += 1;
        }
    }
}
class DecThread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	Counter.count -= 1;
        }
    }
}
public class Main {
  	public static void main(String[] args) throws Exception {
      	Thread t1 = new AddThread();
      	Thread t2 = new DecThread();
      	t1.start();
      	t2.start();
      	t1.join();
      	t2.join();
      	// 多运行几次会发现,count的值不一定是0
      	System.out.println(Counter.count);
    }
}
原子操作
  • 对共享变量进行写入时,必须保证是原子操作
  • 原子操作是指不能被中断的一个或一系列操作

为了保证一系列操作为原子操作:

  • 必须保证一系列操作执行过程中不被其他线程执行。因此可以对操作进行加锁和解锁。
  • Java使用synchronized对一个对象进行加锁
synchronized(lock) {
  	n = n + 1;
}
synchronized简介
特点
  • 性能低:同步代码块会消耗资源
  • 不用担心异常:即使有异常,同步代码块也能释放锁
基本使用
  • 找出修改共享变量的线程代码块
  • 选择一个实例作为锁
  • 使用synchronized(lock){}

对上例进行修改

class Counter {
  	public static int count = 0;
}
class Addthread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	// 加锁
          	synchronized(Main.LOCK) {
              	Counter.count += 1;
            }
        }
    }
}
class DecThread extends Thread {
  	@Override
  	public void run {
      	for(int i=0;i<10000;i++) {
          	// 加锁
          	synchronized(Main.LOCK) {
              	Counter.count -= 1;
            }
        }
    }
}
public class Main {
  	// 定义一个锁
  	public static final Object LOCK = new Object();
  	public static void main(String[] args) throws Exception {
      	Thread t1 = new AddThread();
      	Thread t2 = new DecThread();
      	t1.start();
      	t2.start();
      	t1.join();
      	t2.join();
      	// 多运行几次会发现,count的值始终是0
      	System.out.println(Counter.count);
    }
}
JVM原子操作
类型
  • 基本类型赋值(long和double除外)
int n = 1;
  • 引用类型赋值
List<String> list = aList;

对原子操作不需要进行同步,如果多个操作需要同步,也可以转换成原子操作,如下:

class Pair {
  	int first;
    int last;
    public void set(int first, int last) {
				// 使用同步
      	synchronized(lock) {
            this.first = first;
         		this.last = last;
        }
    }
}

// 不使用同步
class Pair {
  	intp[] pair;
  	public void set(int first, int last) {
      	int[] ps = new int[]{first, last};
      	this.pair = ps;
    }
}
总结

多线程同时修改同一个变量,会造成逻辑错误:

  • 需要通过synchronized同步
  • 同步的本质就是给指定对象加锁
  • 注意加锁对象必须是同一个实例
  • 对JVM定义的单个原子操作不需要同步

synchronized使用详解

加锁对象

加锁对象的选择:把同步逻辑封装到到持有数据的实例中,使用this加锁

synchronized可以用在代码块上,也可以用在方法上,用在方法上表示对整个方法内的代码进行加锁

private int count = 0;
// 对方法加锁
public synchronized void add() {
  	count += 1;
  	count -= 1;
}
// 等同于下面
public void add() {
  	// 对代码块使用当前对象加锁
  	synchronized(this) {
      	count += 1;
      	count -= 1;
    }
}

如果对静态方法进行加锁,那么要使用当前对象的Class实例

public class A {
  	static int count;
  	static void add(int n) {
      	// 锁住的是当前类的Class实例
      	synchronized(A.class) {
          	count += n;
        }
    }
}
读取方法

如果只是单纯的一个原子操作进行读取数据,那么可以不加锁。

public int get() {
  	// 可以进行同步
  	return this.value;
}

但是如果读取过程较复杂存在线程安全问题,则需要进行加锁。

public synchronized int[] get() {
  	int[] result = new int[2];
  	result[0] = this.value[0];
  	// 如果不使用同步,在读取this.value[0]的时候
  	// this.value[1]可能会被其他线程修改
  	result[1] = this.value[1];
  	return result;
}
Thread-safe

如果一个类被设计为允许多线程正确访问的,那这个类就是线程安全的(thread-safe),比如java.lang.StringBuffer

// StringBuffer的方法都使用了synchronized来标识
// ... ...
		@Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }


    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }

    /**
     * @since      1.5
     */
    @Override
    public synchronized void trimToSize() {
        super.trimToSize();
    }

    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @see        #length()
     */
    @Override
    public synchronized void setLength(int newLength) {
        toStringCache = null;
        super.setLength(newLength);
    }
// ... ...

线程安全的类:

  • 不变的类:String,Integer,LocalDate
  • 没有成员变量的类(多是工具类):Math
  • 正确使用synchronized的类:StringBuffer

非线程安全的类:

  • 不能在多线程中共享实例并修改:ArrayList
  • 可以在多线程中以只读方式共享
总结
  • 用synchronized修饰方法可以把整个方法变为同步代码块
  • synchronized方法加锁对象是this
  • 通过合理的设计和数据封装可以让一个类变为“线程安全”
  • 一个类没有特殊说明,默认不是thread-safe
  • 多线程能否访问某个非线程安全的实例,需要具体情况具体分析

死锁

死锁现象

要执行synchronized代码块,必须要先获得指定对象的锁才能运行。

Java的线程锁是可重入的锁,即获取到一个对象的锁的synchronized代码块中再次获取这个对象的锁

public void add(int n) {
  	synchronized(lock) {
      	this.value += n;
      	// 调用另一个加了相同锁的方法
      	addAnother(n);
    }
}
public void addAnother(int m) {
  	// 获取同一个锁
  	synchronized(lock) {
      	this.another += m;
    }
}

// 上面代码等同于
public void add(int m) {
  	synchronized(lock) {
      	this.value += m;
      	synchronized(lock) {
          	this.another += m;
        }
    }
}

也可以是两个不同的锁

public void add(int m) {
  	// 获取lockA的锁
  	synchronized(lockA) {
      	this.value += m;
      	// 获取lockB的锁
      	synchronized(lockB) {
          	this.another += m;
        }// 释放lockB的锁
    }// 释放lockA的锁
}

当不同线程获取多个不同对象的锁可能导致死锁

public void add(int m) {
  	synchronized(lockA) {
      	this.value += m;
      	// 等待获取lockB的锁
      	synchronized(lockB) {
          	this.another += m;
        }
    }
}

public void add(int m) {
  	synchronized(lockB) {
      	this.value += m;
      	// 等待获取lockA的锁
      	synchronized(lockA) {
          	this.another += m;
        }
    }
}

当两个线程分别执行以上代码时,线程A获取到lockA的锁,线程B获取到lockB的锁,然后线程A开始等待获取lockB的锁,而线程B开始等待获取lockA的锁,两个线程陷入互相等待的僵局,旧形成了死锁

死锁形成条件
  • 两个线程各自持有不同的锁
  • 两个线程各自试图获取对方已持有的锁
  • 双方无限等待下去,导致死锁

死锁处理

  • 没有任何机制能解除死锁
  • 只能强制结束JVM进程
避免死锁
  • 多线程获取锁的顺序要一致

wait/notify

synchronized解决了多线程竞争的问题,但没有解决多线程协调的问题,比较典型的生产者消费者问题,消费者必须在有产品时才能消费,如果没有就必须等待

生产者-消费者
// 一个简单的生产者-消费者案例
// 仓库
class Repository {
    private LinkedList<Object> list = new LinkedList<>();
    public synchronized void produce() {
        list.add("1");
        System.out.println("生产一个,当前有" + list.size() + "个");
        // 唤醒所有等待的消费者
        this.notifyAll();
    }
    public synchronized void consume() {
        while (list.size() == 0) {
            try {
                // 消费者开始等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }
        }
        list.remove();
        System.out.println("消费一个,当前有" + list.size() + "个");
    }
}
// 生产者
class Producer extends Thread {
    private Repository repository;
    public Producer(Repository repository) {
        this.repository = repository;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        repository.produce();
    }
}
// 消费者
class Consumer extends Thread {
    private Repository repository;
    public Consumer(Repository repository) {
        this.repository = repository;
    }
    @Override
    public void run() {
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        repository.consume();
    }
}
// 主类
public class Main {

    public static void main(String[] args) {
				// 创建公共仓库
        Repository repository = new Repository();
      	// 创建生产者
        Producer producer1 = new Producer(repository);
        Producer producer2 = new Producer(repository);
        Producer producer3 = new Producer(repository);
				// 创建消费者
        Consumer consumer1 = new Consumer(repository);
        Consumer consumer2 = new Consumer(repository);
        Consumer consumer3 = new Consumer(repository);
				// 开始工作
        producer1.start();
        producer2.start();
        producer3.start();
        consumer1.start();
        consumer2.start();
        consumer3.start();
    }
}

// 运行结果(顺序不固定)
生产一个,当前有1个
消费一个,当前有0个
生产一个,当前有1个
生产一个,当前有2个
消费一个,当前有1个
消费一个,当前有0

可以看出,如果消费者不进行等待,那么会出现消费的产品为空,并且线程会结束,使用wait()进行等待,当仓库中有产品时,生产者再调用notify/notifyAll来唤醒等待的线程,这样消费者就可以继续消费了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陌影~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值