Java多线程(5)--线程同步

线程同步

本文章为个人 “Java零基础实战”学习笔记,仅供参考学习,侵权删

当多个线程并行访问一个共享数据时,可能会导致数据的不准确问题。举例如下:

//统计线程访问量类
class Account implements Runnable{
	private static int n;
	@Override
	public void run(){
		try{
			Thread.currentThread().sleep(1);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		n++;
		System.out.println(Thread.currentThread().getName()+"是当前的第"+n+"位访客");
	}
}

public class Test(){
	public static void main(String[] args){
		Account account = new Account();
		Thread t_A = new Thread(account,"线程A");
		thread t_B = new Thread(account,"线程B");
		t_A.start();
		t_B.start();
	}
}

在这里插入图片描述
结果分析:线程A执行n++,还没有打印访客信息,线程B也执行了n++,所以n=2,两个线程都打印出事第2位访客


线程同步的实现

可以通过synchronized修饰方法来实现线程同步,每个Java对象都有一个内置锁,内置锁会保护用synchronized关键字修饰的方法,要调用该方法必须先获得内置锁,否则处于阻塞状态。


我们对Account类做一下修改

class Account implements Runnable{
	private static int n;
	@Override
	public synchronized void run(){
		try{
			Thread.currentThread().sleep(1);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		n++;
		System.out.println(Thread.currentThread().getName()+"是当前的第"+n+"位访客");
	}
}

再次运行
在这里插入图片描述
代码讲解:线程A先到,获取了run()方法的锁,之后线程B到了,此时run()方法已经被锁起来,要调用必须先拿到锁,线程A调用完方法之后才会释放所,所以A先执行,B再执行,看到的就是正确的结果

  • 给静态方法加synchronized关键字实现同步
public class SynchronizedTest2 {
	public static void main(String[] args) {
		for(int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Runnable() {
				@Override
				public void run() {
					SynchronizedTest2.test();
				} 
			});
			thread.start();
		}
	}
	
	public synchronized static void test() {
		System.out.println("start...");
		try {
			Thread.currentThread().sleep(1000);
		} catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		System.out.println("end...");
	}

}

在这里插入图片描述

如果不加synchronized关键字,start和end不会成对出现,因为在线程thread第一次创建运行时会休眠1000毫秒,这个时间段主线程执行完for循环绰绰有余,静态方法test()也没有加锁,所以会先打印五个start…再打印五个end…

重点:

这里的test()方法是静态方法,如果改为实例方法,则无效,因为线程同步的本质是锁定多个线程所共享的资源,而每个线程都有自己单独的实例方法,相互之间是独立的。实际运行情况是每一个线程都获取自己的锁,然后并行访问,不存在“你运行,我等待”的关系,所以给实例方法添加synchronized关键字修饰并不能实现线程同步

添加synchronized可以同步代码块

public class SynchronizedTest3 {
	public static void main(String[] args) {
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(new Runnable() {
				@Override
				public void run() {
					SynchronizedTest3 synchronizedTest3 = new SynchronizedTest3();
					synchronizedTest3.test();
				}
			});
			thread.start();
		}
	}
	
	//这里也可以是静态方法
	public void test() {
		//不可以是synchronized(this)
		synchronized(SynchronizedTest3.class) {
			System.out.println("start...");
			try {
				Thread.currentThread().sleep(1000);
			} catch (InterruptedException e) {
				// TODO: handle exception
				e.printStackTrace();
			}
			System.out.println("end...");
		}
	}
}

实例对象是每个线程独有的,类则是共享的,所以锁定类就可以实现同步,所以不能是(this)


线程安全的单例模式

单例模式是一种常见的软件设计模式,其核心思想是一个类只有一个实例对象。核心是共享实例对象,那么就把实例对象定义为静态

public class TestDemo{
    private static TestDemo instance;
    private TestDemo(){
        System.out.println("TestDemo...");
    }
    //此处加synchronized就会常见一个对象,否则创建两个,可以从运行结果看出来
    public synchronized static TestDemo getInstance(){
        if(instance == null){
            instance = new TestDemo();
        }
        return instance;
    }
    
    public static void main(String[] args){
        new Thread(new Runnable(){
            @Override
            public void run(){
                TestDemo instance = TestDemo.getInstance();
            }
        }).start();
        TestDemo instance2 = TestDemo.getInstance();
    }
}
  • 若getInstance()方法不加synchronized关键字修饰,就会创建两个实例对象,原因是线程1和线程2是并行访问的,线程1先来判断instancenull是成立的,然后线程1来实例化对象,正在此时,实例化对象的操作还没有完成,线程2来了。先判断 instancnull 是成立的,于是线程2 也执行了实例化对象的操作,所以导致实例化了两个对象

  • synchronized可以修饰方法,也可以修饰代码块,下面通过同步代码块的方式来实现单例模式
public class SingletonDemo2 {
	private volatile static SingletonDemo2 instance;
	private SingletonDemo2() {
		System.out.println("SingletonDemo2");
	}
	public static SingletonDemo2 getInstance() {
		if(instance == null) {
			synchronized(SingletonDemo2.class) {
				if(instance == null) {
					instance = new SingletonDemo2();
				}
			}
		}
		return instance;
	}
	
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				SingletonDemo2 instance = SingletonDemo2.getInstance();
			}
		}).start();
		SingletonDemo2 instance2 = SingletonDemo2.getInstance();
	}
}

代码讲解:这里使用volatile关键字修饰instance,volatile的作用是可以使内存中的数据对线程可见。Java的内存模式:一个线程在访问内存数据时,其实不是拿到该数据本身,而是将该数据复制保存到工作内存中。相当于取出一个副本,对工作内存中的数据进行修改,再保存到内存中,即主内存对线程是不可见的。当线程1拿到锁,并锁定整个类之后,就是李华了instance对象,但此时的instance是工作内存中的数据,还需要讲工作内存中的数据存到住内存中。然而锁定的知识实例化的步骤,保存到主内存的步骤没有加锁。所以工作内存中的instance完成实例化之后,还未更新到主内存就释放了锁,线程2立即获取锁,又从主内存中复制了一份数据,此时的数据还是null。线程2又在工作内存中完成了一次实例化。然后线程1和线程2再将他们各自实例化的数据保存到主内存中


死锁

  • 什么是死锁?(举例)

假设10个人围一桌吃饭,但是每个人只有一根筷子,要求必须凑齐一双筷子才可以吃菜。如果把每个人看成一个线程,筷子就是线程要获取的资源,现在每个线程都占用一个资源并且不愿意释放,而且任意一个线程想继续执行就必须获取其他线程的资源,那么所有的线程都处于阻塞状态,程序无法向下执行也无法结束。

  • 如何破解死锁?

唯有某个线程愿意做出让步,贡献自己的资源给其他线程使用。获取到资源的线程就可以执行自己垫付业务方法,执行完毕后会释放它锁占有的两个资源。

  • 死锁代码举例
class Chopsticks{
    
}

class DeadLockRunnable implements Runnable{
    public int num;
    private static Chopsticks chopsticks1 = new Chopsticks();
    private static Chopsticks chopsticks2 = new Chopsticks();
    @Override
    public void run(){
        if(num == 1){
            System.out.println(Thread.currentThread().getName()+"获取到 chopsticks1,等待获取chopsticks2");
            synchronized (chopsticks1){
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (chopsticks2){
                    System.out.println(Thread.currentThread().getName()+"用餐完毕");
                }
            }
        }
        if(num == 2){
            System.out.println(Thread.currentThread().getName()+"获取到 chopsticks2,等待获取chopsticks1");
            synchronized (chopsticks2){
                try{
                    Thread.sleep(100);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (chopsticks1){
                    System.out.println(Thread.currentThread().getName()+"用餐完毕");
                }
            }
        }
    }
}

public class DeadLockTest{
    public static void main(String[] args){
        DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
        deadLockRunnable1.num = 1;
        DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
        deadLockRunnable2.num = 2;
        new Thread(deadLockRunnable1,"张三").start();
        new Thread(deadLockRunnable2,"李四").start();
    }
}

重入锁

  • 什么是重入锁?

重入锁(ReentrantLock)是对synchronized的升级,synchronized是通过 JVM 实现的,ReentrantLock 是通过 JDK 实现的。

  • 重入锁有什么特点呢?

重入锁指可以给同一个资源添加多个锁,并且解锁的方式与synchronized也不同,synchronized的锁是线程执行完毕之后会自动释放,ReentrantLock 的锁必须手动释放,可以通过 ReentrantLock 实现访问量统计

class Account3 implements Runnable{
	private static int num;
	private ReentrantLock reentrantLock = new ReentrantLock();
	@Override
	public void run() {
		reentrantLock.lock();
		num++;
		System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
		reentrantLock.unlock();
	}
}

public class ReentrantLockTest {
	public static void main(String[] args) {
		Account3 ac = new Account3();
		Thread thread1 = new Thread(ac,"线程1");
		Thread thread2 = new Thread(ac,"线程2");
		thread1.start();
		thread2.start();
	}
}

效果与 synchronized 一样,在此基础上可以添加多把锁,只需要多次调用lock()方法即可。

class Account3 implements Runnable{
	private static int num;
	private ReentrantLock reentrantLock = new ReentrantLock();
	@Override
	public void run() {
		reentrantLock.lock();
		reentrantLock.lock();
		num++;
		System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
		reentrantLock.unlock();
		reentrantLock.unlock();
	}
}

重点:我们说过 ReentrantLock 需要手动解锁,如果我们只加锁而不解锁,其他线程将无法获得资源,程序无法继续执行;所以需要注意我们加了几把锁就必须释放几把锁


生产者消费者模式

  • 生产者消费者意为在一个生产环境中,生产者和消费者在同一个时间段内共享同一块缓冲区。生产者负责向缓冲区添加数据,消费者负责从缓冲区中取出数据。以生产汉堡和消费汉堡为例来实现生产者消费者模式
//汉堡类
public class Hamburger {
	private int id;
	
	public void setId(int id) {
		this.id = id;
	}
	public int getId() {
		return id;
	}
	public Hamburger(int id) {
		this.id = id;
	}
	@Override
	public String toString() {
		return "Hamburger [id=" + id + "]";
	}
}


//装汉堡的容器类
public class Container {
	public Hamburger[] array = new Hamburger[6];
	public int index = 0;
	//向容器中添加汉堡
	public synchronized void push(Hamburger hamburger) {
		while(index == array.length) {
			try {
				this.wait();
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.notify();
		array[index] = hamburger;
		index++;
		System.out.println("生产了一个汉堡:" + hamburger);
	}
	
	//从容器中取出汉堡
	public synchronized Hamburger pop() {
		while(index == 0) {
			try {
				this.wait();
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.notify();
		index--;
		System.out.println("消费了一个汉堡:" + array[index]);
		return array[index];
	}
}


//生产者类
public class Producer implements Runnable {
	private Container container = null;
	public Producer(Container container) {
		this.container = container;
	}
	@Override
	public void run() {
		for(int i = 0; i < 30; i++) {
			Hamburger hamburger = new Hamburger(i);
			this.container.push(hamburger);
			try {
				Thread.currentThread().sleep(1000);
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}


//消费者类
public class Consumer implements Runnable {
	private Container container = null;
	public Consumer(Container container) {
		this.container = container;
	}
	@Override
	public void run() {
		for(int i = 0; i < 30; i++) {
			this.container.pop();
			try {
				Thread.currentThread().sleep(1000);
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

//测试类
public class Test {
	public static void main(String[] args) {
		Container container = new Container();
		Producer producer = new Producer(container);
		Consumer consumer = new Consumer(container);
		new Thread(producer).start();
		new Thread(producer).start();
		new Thread(consumer).start();
		new Thread(consumer).start();
		new Thread(consumer).start();
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值