Java并发05-线程同步(万字详解建议收藏)

1.使用场景

多个线程访问同一个资源,多个线程可能会产生不一致。为了解决此问题,需要线程同步。调用某个方法时,该资源是被一个线程独占,不能被其他线程访问。
不能出现一个线程在访问一个资源的时候被另一个线程打断

MyThread m = new MyThread();
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
t1.start();
t2.start();

这是两个线程,如果run方法中调用了一个类的一个方法,那么这两个线程会如何进行呢?
两个线程执行时访问了同一个对象的同一个方法,那么该方法资源被两个线程共享,两个线程交替执行该方法。
一个线程修改了该方法的变量的值,下一个线程再调用这个方法会接着第一个线程的最终结果继续调用。
但这不是我们想要的结果。问题出在第一个线程执行这个方法过程中被第二个线程打断了,执行这个方法就应该从头到尾地直至执行完毕(原子性地执行)
sleep方法可能显著地体现出打断的效果,但是没有sleep方法也有可能产生这种问题。
如果解决?
在一个线程访问对象时,将该对象锁住,只能被该线程使用而不能被其他线程使用。

2.线程同步的两种方法

2.1 线程同步方法1-synchronized

在这里插入图片描述
在这里插入图片描述
关键字synchronized,作用是保证一个线程进入锁定的区域之后,在执行的过程之中,不会被其他线程打断。之后该类就是线程安全的
synchronized的简单示例程序:
未加锁

public class test implements Runnable {
	timer t = new timer();
	public static void main(String []args){
		test T = new test();
		Thread thread1 = new Thread(T);
		Thread thread2 = new Thread(T);
		thread1.setName("t1");
		thread2.setName("t2");
		thread1.start();
		thread2.start();
	}
	public void run() {
		t.add(Thread.currentThread().getName());
	}
}
class timer {
	private static int num = 0;
	public void add (String name){
		num++;
		try {
			Thread.sleep(1);
		}catch (InterruptedException e){}
		System.out.println(name+ ",你是第"+num+"个使用timer的线程");
	}
}

输出结果:
t1,你是第2个使用timer的线程
t2,你是第2个使用timer的线程

线程t1执行add,执行完++后睡眠;线程t2打断t1,执行add方法,执行完++后睡眠;t1醒来,打断t2,执行输出,线程t1执行完毕;线程t2醒来,执行输出。

注意,synchronized的作用是使一个线程可以完整地执行一个方法。如果两个线程访问同一个对象的同一个方法,那么不会为每个线程复制一份,而是公用同一个对象,它们的成员变量也是相同的,加了锁的之后不代表每个线程调用都是一个全新的方法和成员变量,而是不会产生打断,第二个线程在第一个线程执行完这个方法后仍会继续第一个线程保留的成员变量的值继续执行。 举例见以下代码:
注意这个锁允许其他的线程调用这个对象的未锁定的方法,并且在执行时会参照已锁定方法运行时修改的成员变量值。

加了锁

class timer {
	private static int num = 0;
	public synchronized void add (String name){}
输出结果:
t1,你是第1个使用timer的线程
t2,你是第2个使用timer的线程

结果是2而不是1代表了第二个线程不是重新执行这个方法,而是接着第一个线程执行过后的结果继续执行

下面来解释synchronized的具体用法:
synchronized锁定的是对象而非代码,所处的位置是代码块或方法

1. 一种使用方法是对代码块使用synchronized关键字

public void fun(){
synchronized (this){ }
}
括号中填写的是锁定的对象或类Class
如果是this,表示在执行该代码块时锁定当前对象,其他线程不能调用该对象的该代码块,但可以调用其他对象的所有方法(包括锁定的代码块),也可以调用该对象的未锁定的代码块或方法。
如果是Object o1,表示执行该代码块的时候锁定该对象,其他线程不能访问该对象(该对象是空的,没有方法,自然不能调用)
如果是类.class,那么锁住了该类的Class对象,只对静态方法生效。
这种方式注意可以出现锁住的对象不正确和锁住的代码块不正确。

示例:银行的存取系统

class Account {
	String name;
	float balance;
	Account(String name,float balance){
		this.name = name;
		this.balance = balance;
	}
	public void deposit(float in){
		balance += in;
	}
	public void withdraw(float out){
		balance -=out;
	}
	public float getBalance(){
		return balance;
	}
}
class AccountOperator implements Runnable{ 
//每一个AccountOperator对象都独享一个Account对象
	private Account account = null;
	AccountOperator(Account account){
		this.account  = account;
	}
	public void run() {
		synchronized(account){
			account.deposit(500);
			account.withdraw(800);
			System.out.println(Thread.currentThread().getName()+":"+ account.balance);
		}
	}	
}
public class TestAccount{
	public static void main(String []args){
		Account account = new Account("aaa",2000);//只有一个Account对象
		AccountOperator ao = new AccountOperator(account);
//只有一个AccountOperator对象
		Thread t[] = new Thread[5];
		for(int i = 0;i<5;i++){
			t[i] = new Thread(ao,"Thread"+i);
//每个线程都执行的是ao这个对象的run方法,由于这个方法调用时会锁定account对象,所以必须存取方法执行完毕后,其他线程才能调用run方法,获得account对象的锁,继续执行存取操作。AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。

从输出结果也可以看出,每个线程执行完毕后余额都固定减少300
			t[i].start();
		}
	}
}
输出结果:
Thread0:1700.0
Thread4:1400.0
Thread3:1100.0
Thread2:800.0
Thread1:500.0

** 2. 另一种写法是将synchronized作为方法的修饰符**

public synchronized void fun() {} //这个方法执行的时候锁定该当前对象
每个类的对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的一个对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

如果synchronized修饰的是静态方法,那么锁住的是这个类的Class对象。没有其他线程可以调用该类的这个方法或其他的同步静态方法。
示例:思考加了synchronized之后该程序是否就能达到预期效果

class Sync implements Runnable {  
    public synchronized void run() {  
        System.out.println("test开始..");  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("test结束..");  
} 
}  
public class Test {  
    public static void main(String[] args) {  
        for (int i = 0; i < 3; i++) {  
            Thread thread = new Thread(new Sync() ); 
 //三个线程,每个线程对应一个新的对象
            thread.start();  
        }  
    }  
}
运行结果:
test开始..
test开始..
test开始..
test结束..
test结束..
test结束..

解释:主方法new了三个Thread对象,每个Thread对象中new了一个Sync对象
这表示这三个线程分别执行的是不同的对象。每个线程在执行当前对象锁定了的run方法时,此时每个线程各自执行当前对象的run方法,而没有调用同一个对象的run方法。这是该锁是无效的。

实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的这个代码段。
synchronized锁住的是括号里的对象,而不是代码。对于非静态的synchronized方法,锁的就是对象本身也就是this。
当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。
所以我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。
——————————————————————————
如果想达到预期目的,那么应该是三个线程执行同一个对象,那么一个线程执行该对象锁住的方法时,其他线程不能执行该方法。
代码如下:

class Sync implements Runnable {  // Sync类也可以写为这样
    public void run() {  
		synchronized(this){//执行该代码块时锁定当前的对象
	        System.out.println("test开始..");  
	        try {  
	            Thread.sleep(1000);  
	        } catch (InterruptedException e) {  
	            e.printStackTrace();  
	        }  
	        System.out.println("test结束.."); 
		}
    } 
public class Test{  
    public static void main(String[] args) {
    	Sync s = new Sync();//同一个对象
        for (int i = 0; i < 3; i++) {  
            Thread thread = new Thread(s);  
            thread.start();  
        }  
    }  
}
输出结果:
test开始..
test结束..
test开始..
test结束..
test开始..
test结束..

如果想将第一个程序实现:当执行一个对象的锁住的方法或代码块时,该类所有其他对象都不能执行该方法或代码块(从锁定当前对象扩大到锁定该类的所有对象)有2种方法:
1.synchronized修饰的静态方法锁定的是这个类的所有对象
public static synchronized void fun(){} //执行一个对象该方法时,其他线程对应的Sync对象都不能执行该方法。
2.

	class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

比如 synchronized(Sync.class){执行该代码块时锁定该类所有对象}
此时无论是同时执行多少个Sync对象,都只能有一个对象调用该加锁的代码块。
synchronized(Sync.class)实现了全局锁的效果

2.2 线程同步方法2-ReentrantLock

在这里插入图片描述
一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
在这里插入图片描述
第二个构造方法是构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是公平机制会大大降低想能。所以默认锁是没有公平机制的。
在这里插入图片描述
在这里插入图片描述

主要使用lock和unlock方法来实现同步
在lock和unlock之间的代码块(称为临界区)是同步的,在同一时刻只能由一个线程执行
当其他线程试图获取锁时,它们被阻塞,直到持有锁的线程释放锁。

ReentrantLock myLock = new ReentrantLock();
myLock.lock();
try{
}finally{
	mylock.unlock();
}

将释放锁的代码放在finally中是非常重要的,因为如果在临界区抛出异常,那么锁必须被释放,否则其他线程会一直阻塞

注意!
如果使用锁,就无法使用带资源的try语句。因为没有finally就无法在合适的地方释放锁

示例代码:

public class Bank {
	private Lock bankLock = new ReentrantLock();
	
	//...
	public void transfer(int from,int to,int amout) {
		bankLock.lock();
		try {
			System.out.println("进入转账");
		} finally {
			System.out.println("转账结束");
			bankLock.unlock();
		}
	}
	public static void main(String[] args) {
		Bank bank = new Bank();
		bank.transfer(0, 0, 0);
	}
}

注意每一个Bank对象都有自己的ReentrantLock对象
如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞。

锁是可重入的(reentrant),因为线程可以重复获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
比如在转账时(锁住)打印总额(锁住),就获得了锁两次
条件对象
线程进入临界区(同步块)时,发现必须要满足一定条件才能执行。要使用一个条件对象来管理那些已经获得一个锁但是不能做有用工作的线程
条件对象也称为条件变量
一个锁对象可以有多个相关的条件对象,newCondition方法可以获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。
当发现条件不满足时
调用Condition对象的await方法
此时线程被阻塞,并放弃了锁。等待其他线程的相关操作使得条件达到满足
注意!
等待获得锁的线程和调用await方法的线程有本质区别。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞,相反,它处于阻塞状态,直到另一个线程调用同一个条件的signalAll方法为止
await方法和signalAll方法是配套使用的
await进入等待,signalAll解除等待

signalAll方法会重新激活因为这一条件而等待的所有线程。当这些线程从等待集中移出时,它们再次成为可运行的,线程调度器将再次激活它们。同时它们将试图重新进入该对象。一旦锁可用,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行

线程应该再次测试该条件。由于无法确保该条件被满足,signalAll方法仅仅是通知正在等待的线程,此时有可能满足条件,值得再次去检测该条件
对于await方法的调用应该用在这种形式:

while(!(ok to continue)){
	condition.await();
}

最重要的是需要其他某个线程调用signalAll方法。当一个线程调用await方法,它没有办法去激活自身,只能寄希望于其他线程。如果没有其他线程来激活等待的线程,那么就会一直等待,出现死锁。
如果所有其他线程都被阻塞,且最后一个线程也调用了await方法,那么它也被阻塞,此时程序挂起

当对象的状态有利于等待线程的条件满足时,应该调用signalAll方法

注意!
signalAll方法不会立刻激活一个等待的线程,仅仅是解除等待线程的阻塞,以便这些线程可以在当前线程(调用signalAll方法的线程)退出时,通过竞争来实现对对象的方法
这个await和signalAll方法的组合类似于Object对象的wait和notifyAll方法的组合
API:
在这里插入图片描述
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。
在这里插入图片描述
在这里插入图片描述
示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Bank{
	
	private final double[] accounts;
	private Lock lock;
	private Condition sufficientFunds;
	
	public Bank(int count,double initBalance) {
		accounts = new double[count];
		for (int i = 0; i < accounts.length; i++) {
			accounts[i] = initBalance;
		}
		lock = new ReentrantLock();
		sufficientFunds = lock.newCondition();
	}public void transfer(int from ,int to,int amount) throws InterruptedException {
		lock.lock();
		try {
			while(accounts[from] < amount) {
				sufficientFunds.await();
			}
			accounts[from] -= amount;
			accounts[to] += amount;
			System.out.println("总余额为"+getTotalBalance());
			sufficientFunds.signalAll();
		}finally {
			lock.unlock();
		}
	}
	public double getTotalBalance() {
		lock.lock();
		try {
			double sum = 0;
			for (double d : accounts) {
				sum+=d;
			}
			return sum;
		}finally {
			lock.unlock();
		}
	}
}

注意和synchronized关键字的区别:
synchronized的局限性
不能中断一个正在试图获得锁的线程
试图获得锁时不能设定超时
每个锁仅有单一条件,可能不够使用

应该使用synchronized关键字还是Lock/Condition ?
最好二者都不用,可以使用阻塞队列来同步完成一个共同任务的线程
synchronized编写的代码量较少,可以减少出错几率
如果特别需要使用Lock/Condition结构的特性时,才使用

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值