首页 > 其他分享 >多线程的“锁”

多线程的“锁”

时间:2023-08-17 10:46:16浏览次数:40  
标签:Thread synchronized int 线程 new 多线程 public

其实,很多初学者(包括我自己)初期学习多线程时都被视频带偏了...虽然我始终认为培训班的视频是最适合非科班零基础入门的,但是在多线程方面,无一例外都讲得比较糟糕。

感触很深的一点是:很多新手觉得多线程难,并不是因为volatile、ReentrantLock或者Executor线程池,而是从一开始就没弄明白“什么是锁”,导致后面根本学不进去。

  • 什么是“锁”?
  • 锁到底长啥样?
  • 它锁定的是代码吗?

在我看来,这个问题不搞清楚,后面的内容根本学不明白。而一旦搞清楚这些概念,后面很多问题其实也就迎刃而解。

内容介绍:

  • 线程安全问题与解决办法
  • 锁到底长啥样
  • 关于锁的几个案例
  • 面试题:写一个固定容量的同步容器

线程安全问题与解决办法

在上一篇结尾,我们说Java两种创建多线程的方法中,一般推荐实现Runnable接口的方式。主要原因可以归结为:

  • 资源和线程分离,更加面向对象
  • 可以做到资源共享

而所谓的线程安全问题可以粗浅地理解为“数据不一致”。但单纯的资源共享并不一定会导致线程安全问题。当同时满足以下三个条件时,才可能引发线程安全问题。

  • 多线程环境
  • 有共享数据
  • 有多条语句操作共享数据/单条语句本身非原子操作

来看一段 

@养兔子的大叔  在(JDK)ReetrantLock手撕AQS一文中关于线程安全的示例代码:

 

public class ThreadForIncrease {
    static int cnt = 0;  //共享数据cnt
    public static void main(String[] args) {
         Runnable r = new Runnable() {
            @Override
            public void run() {
                //有多条语句操作共享数据
                int n = 10000;
                while(n>0){
                    cnt++;
                    n--;
                }
            }
        };
        //多线程环境
        Thread t1  = new Thread(r);
        Thread t2  = new Thread(r);
        Thread t3  = new Thread(r);
        Thread t4  = new Thread(r);
        Thread t5  = new Thread(r);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        try {
            //等待足够长的时间 确保上述线程均执行完毕
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cnt);
    }

}

//输出的结果会小于50000

很明显,上面示例完全符合“线程安全问题”的三个条件。

出现问题的原因在于cnt++并不是原子性操作,实际上分三步:

  1. 各个线程从主存拷贝变量
  2. 在自己的工作内存进行+1操作
  3. 把结果回写到主存
5个线程总共执行50000次,如果发生多次上面的情况,比如99重复回写,200重复回写,那最终结果就是49998

如何解决?仔细回想一下三个条件:

  • 多线程环境(这个是前提,无法改变,没有多线程当然没有安全问题)
  • 有共享数据(通常无法改变,特定情境下必须要操作共享数据)
  • 非原子性操作(可以改变!)

所以经过分析,我们能优化的只有第三点:把对共享数据的操作变成原子性操作。针对上面的情况解决办法有多种,比如cnt使用原子类AutomicInteger,或者加锁等等。这里演示加锁的情况(其实这种情况加锁有点下药过猛了)。

//使用synchronized实现多线程累加操作
public class synchronizedForIncrease {
    static int cnt = 0;
    public static void main(String[] args) {
         Runnable r = new Runnable() {
            @Override
            public synchronized void run() {//同步方法(synchronized加锁)
                int n = 10000;
                while(n>0){
                    cnt++;
                    n--;
                }
            }
        };
        Thread t1  = new Thread(r);
        Thread t2  = new Thread(r);
        Thread t3  = new Thread(r);
        Thread t4  = new Thread(r);
        Thread t5  = new Thread(r);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        try {
            //等待足够长的时间 确保上述线程均执行完毕
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cnt);
    }

}
//输出结果将和预想中的一致:50000

用synchronized修饰run()方法后,就相当于将方法内的多个语句捆绑在一起,要么全部执行,要么尚未开始,不会出现“执行到一半被挂起”的情况,也就避免了线程安全问题的发生。


锁到底长啥样

其实“锁”本身是个对象,且理论上可以是任意对象。synchronized这个关键字不是“锁”,硬要说的话,加synchronized仅仅是相当于“加锁”这个操作,真正的锁是“某一个对象”。

所以,所谓的加锁,严格意义上不是锁住代码块!如果这样想的话,后面很多问题就没法解释了。

补充几个概念:

  • 互斥的最基本条件是:共用同一把锁
  • 静态方法的锁是所在类的字节码对象:xxx.class对象,普通方法的锁是this对象
  • 针对同一个线程,synchronized锁是可重入的

下面通过几个小案例,帮大家加深对上面三句话的理解


关于锁的几个案例

  • 同一个类中的synchronized method m1和method m2互斥吗?
t1线程执行m1方法时要去读this对象锁,但是t2线程并不需要读锁,两者各管各的,没有交集(不共用一把锁)

 

  • 同一个类中synchronized method m1中可以调用synchronized method m2吗?
synchronized是可重入锁,可以粗浅地理解为同一个线程在已经持有该锁的情况下,可以再次获取锁,并且会在某个状态量上做+1操作

 

  • 子类同步方法synchronized method m可以调用父类的synchronized method m吗(super.m())?
子类对象初始化前,会调用父类构造方法,在结构上相当于包裹了一个父类对象,用的都是this锁对象

 

  • 静态同步方法和非静态同步方法互斥吗?
各玩各的,不是同一把锁,谈不上互斥

面试题:写一个固定容量的同步容器

据说是淘宝?很久以前的一道面试题:

面试题:写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用

wait/notifyAll实现:

public class MyContainer1<T> {
	final private LinkedList<T> lists = new LinkedList<>();
	final private int MAX = 10; //固定容量,假定最多10个元素
	private int count = 0;
	
	//put方法
	public synchronized void put(T t) {
		while(lists.size() == MAX) { //想想为什么用while而不是用if?
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		lists.add(t);
		++count;
		this.notifyAll(); //通知消费者线程进行消费
	}
	
        //get方法
	public synchronized T get() {
		T t = null;
		while(lists.size() == 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		t = lists.removeFirst();
		count --;
		this.notifyAll(); //通知生产者进行生产
		return t;
	}
	
	public static void main(String[] args) {
		MyContainer1<String> c = new MyContainer1<>();
		//启动消费者线程
		for(int i=0; i<10; i++) {
			new Thread(()->{
				for(int j=0; j<5; j++) 
                                  System.out.println(c.get());
			}, "c" + i).start();
		}
		
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		//启动生产者线程
		for(int i=0; i<2; i++) {
			new Thread(()->{
				for(int j=0; j<25; j++) 
                                  c.put(Thread.currentThread().getName() + " " + j);
			}, "p" + i).start();
		}
	}
}

对于初学者,这个面试题的难点在于:

 

首先,能想到在MyContainer中塞入LinkedList作为容器(因为有removeFirst方法,比较方便)。Java集合体系中,已经提供了足够多的容器,我们如果要模拟自己的容器,一般会选择将现有的容器包装进自己的容器中,而不是去自己实现一个容器。

 

其次,wait方法必须配合notifyAll。据说《Effective Java》甚至提出,wait在绝大多数场景下应该伴随着notifyAll而不是notify。因为notify的唤醒是随机,不能确定唤醒的是哪个线程(可能是消费者方,也可能是生产者方)。所以当某个生产者线程生产完第10个商品让出执行权后,下次抢到执行权的可能还是生产者方的其他线程(触发lists.size()==MAX条件),这样全部生产者线程就会等待(在此之前消费者线程也已经全部等待),整个程序就会发生死锁:

第⑤步只是举个例子,实际上也有可能是唤醒消费者,因为notify的唤醒是随机的

如果还是有同学不明白为什么生产者线程最终会全部等待,可以看看下面的例子,虽然不够贴切,但是以我的美术功底,尽力了:

如果是notifyAll,则会唤醒所有线程,且各个线程抢到执行权的概率是一致的。即使下一次还是生产者线程抢到执行权并且等待了,此时还有其他线程是活着的。

最后,由于理论上锁可以是任意对象,所以锁的wait/notify/notifyAll等方法就被定义在Object类中,让所有类去继承。如果你仍觉得synchronized才是锁,这个问题是解释不通的。所以,请明确,wait/notify/notifyAll这些方法都是锁对象的方法,线程之所以会产生等待、唤醒等一系列状态,都是去读取锁对象时被指定的。

wait notify notifyAll

 

最后,提供ReentrantLock实现的版本,更为简单,而且可以精确唤醒生产者线程/消费者线程:

public class MyContainer2<T> {
	final private LinkedList<T> lists = new LinkedList<>();
	final private int MAX = 10; //最多10个元素
	private int count = 0;
	
	private Lock lock = new ReentrantLock();
	private Condition producer = lock.newCondition();
	private Condition consumer = lock.newCondition();
	
	public void put(T t) {
		try {
			lock.lock();
			while(lists.size() == MAX) { //想想为什么用while而不是用if?
				producer.await();
			}
			
			lists.add(t);
			++count;
			consumer.signalAll(); //通知消费者线程进行消费
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public T get() {
		T t = null;
		try {
			lock.lock();
			while(lists.size() == 0) {
				consumer.await();
			}
			t = lists.removeFirst();
			count --;
			producer.signalAll(); //通知生产者进行生产
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
		return t;
	}
	
	public static void main(String[] args) {
		MyContainer2<String> c = new MyContainer2<>();
		//启动消费者线程
		for(int i=0; i<10; i++) {
			new Thread(()->{
				for(int j=0; j<5; j++) System.out.println(c.get());
			}, "c" + i).start();
		}
		
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		//启动生产者线程
		for(int i=0; i<2; i++) {
			new Thread(()->{
				for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
			}, "p" + i).start();
		}
	}
}

 

按照惯例,还是留一道思考题,是我之前面试被考到的,咋一听有点懵,其实本质是一样的。始终抓住锁的本质即可迎刃而解:

一个对象的get/set方法如果加上synchronized,t1访问get方法,t2访问set方法,这两个线程互斥吗?

标签:Thread,synchronized,int,线程,new,多线程,public
From: https://www.cnblogs.com/jiaodaoniujava/p/17636969.html

相关文章

  • (随笔)龟兔赛跑(多线程调用同一资源时一个线程结束时其他线程保持运行)
    问题:当其一线程结束运行后其他线程保持运行而非结束现象:控制台会输出两次thewinneris:xxx代码如下packagecom.demo01;/***TODO模拟归途赛跑**@authorpangyangjian*@since2023/8/1616:10*/publicclassTextThread_5implementsRunnable{ @Override......
  • 多线程&异步编程
    多线程&网络编程(异步编程)1)重要性,高并发,短时间内遇到大量请求2)难度硬件.操作系统多线程本身的复杂性,死锁,资源抢占,线程同步...--->多线程进程:一般指程序中运行的程序,实际作用是为程序在执行过程中创建好所需要的环境和资源.线程:是进程的一个实体,是Cpu用来调度......
  • 多线程|线程的特性
      ......
  • 用 TaskCompletionSource 来做多线程间的数据同步
    publicabstractclassHunClientBase{protectedComunicationConfig_ComunicationConfig;protectedHubConnection_HubConnection;privateTaskCompletionSource<string>requestCompletionSource;protectedHunClientBas......
  • Java并发编程:实现高效、线程安全的多线程应用
    Java并发编程是开发高效、可扩展的多线程应用的关键。合理地利用多线程可以提高程序的性能和响应性,但同时也会引入线程安全的问题。本文将介绍Java并发编程的关键技巧,帮助读者实现高效、线程安全的多线程应用。 线程安全的数据结构和类Java提供了许多线程安全的数据结构和类,如Co......
  • 什么是多线程中的上下文切换
    多线程中的上下文切换(ContextSwitching)是指在多任务(多线程)环境下,操作系统将当前线程的状态保存(上下文信息,如寄存器内容、程序计数器等)并切换到另一个就绪状态的线程的过程。上下文切换是实现多任务并发的基本机制之一,它允许多个线程在一个物理CPU上交替执行,从而实现了多线程并......
  • Java并发编程:实现高效、线程安全的多线程应用
    Java并发编程是开发高效、可扩展的多线程应用的关键。合理地利用多线程可以提高程序的性能和响应性,但同时也会引入线程安全的问题。本文将介绍Java并发编程的关键技巧,帮助读者实现高效、线程安全的多线程应用。 线程安全的数据结构和类Java提供了许多线程安全的数据结构和类,如Co......
  • 中电金信:技术实践|Flink多线程实现异构集群的动态负载均衡
    导语:ApacheFlink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。本文主要从实际案例入手并结合作者的实践经验,向各位读者分享当应用场景中异构集群无法做到负载均衡时,如何通过Flink的自定义多线程来实现异构集群的动态负载均衡。●1. 前言●2. 出现的问......
  • 多线程1
    多线程1 8.1基本概念:程序、进程、线程程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期......
  • 多线程
    多线程1.什么是线程它是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。而多线程是为了充分利用cpu资源提高效率2.如何在java中创建多线程它有两种方式:继承Thread类publicclassMyThreadextendsThread{@Overridepublic......