Java线程学习(二)-----线程同步

线程为什么要使用同步?

当一个Java项目运行的时候,Java是支持多线程并发的,当多个线程同事访问一个可共享资源的时候,将会导致数据的bu不准确,因此加入同步锁来避免该线程没有执行结束前被别的线程调用,达到变量的唯一性和准确性。

实现线程同步的方法(7种方式):

一、同步方法 

    即有synchronized关键字修饰的方法。 
    由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 
    内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

    注意:该方法也可以修饰静态方法,此时如果调用该静态方法,将会锁住这个类

public synchronized void xxx(){}

二、 同步代码块

即有synchronized关键字来修饰的语句块

被该关键字修饰的代码块会自动被加上内置锁,从而实现同步

注意:同步是一种高开销的方法,应尽量减少代码块的内容

 synchronized(object){ .... }

 举个例子:

package Thread.study.sync;

public class BankDemo {
	public static void main(String[] args) {
		Bank bank =  new Bank();
		BankThread thread1 = new BankThread(bank);
		thread1.start();
		BankThread thread2 = new BankThread(bank);
		thread2.start();
	}
}

class BankThread extends Thread{
	private Bank bank;

	public BankThread(Bank bank) {
		super();
		this.bank = bank;
	}

	@Override
	public void run() {
		System.out.println("取款"+bank.getMoney(400));
	}
}
class Bank{
	private int money = 500;	 
	public synchronized int getMoney(int number) {
		if(number < 0) {//判断取款金额是否正确
			return -1;
		}else if(money < 0) {//判断账户里面的余额是否正确
			return -2;
		}else if(number > money) {//判断取款金额是否超过存折里面的金额
			return -3;
		}else {
			try {
				Thread.sleep(1000);//休眠1秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
		money-=number;//取钱之后存款的余额
		System.out.println("余额"+money);//输出余额
		return number;
	}
}

结果: 

余额100
取款400
取款-3

当去掉关键字synchronized,线程数据就会出错。

3、使用特殊域变量(volatile)实现线程同步

  • volatile为域变量提供了一种免锁机制
  • 该关键字修饰域相当于告诉虚拟机该域可能会被其他线程更新
  • 所以每次使用该域就要重新计算,而不是重新使用寄存器中的值
  • 不能够修饰final类型的变量,也不会提供任何的原子性操作

(多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题)不是很明白,先记下以后再探讨

代码:

private volatile int money = 500;	 
public int getMoney(int number) {
	if(number < 0) {//判断取款金额是否正确
		return -1;
	}else if(money < 0) {//判断账户里面的余额是否正确
		return -2;
	}else if(number > money) {//判断取款金额是否超过存折里面的金额
		return -3;
	}else {
		try {
			Thread.sleep(1000);//休眠1秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
	money-=number;//取钱之后存款的余额
	System.out.println("余额"+money);//输出余额
	return number;
}

 注意:上面的代码只是举例子,但是实际输出结果并不是自己想要的,volatile这个关键字实际用法场景需要研究。

4、使用重入锁实现线程同步

在JavaSE5.0中新增一个java.util.concurrent包来支持同步。ReentrantLock类可重入、互斥、实现了lock接口的锁。它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力

ReentrantLock类常用方法:

  • ReentrantLock() : 创建一个实例对象
  • lock:获取锁
  • unlock:释放锁

lock对象和synchronized的使用场合:

  1. 虽然俩个都是实现线程同步的,但是他们对程序来说开销大,所以最好的情况就是都不使用,使用java.util.concurrent包提供的机制, 能够帮助用户处理所有与锁相关的代码。 
  2. synchronized能够简化代码。
  3. 如果需要更高级的功能,就用lock对象,注意要及时释放掉锁,避免出现死锁的情况。

https://www.cnblogs.com/null-qige/p/9337656.html  这个里面讲解了java.util.concurrent)

代码:

private int money = 500;	 
	
private Lock lock = new ReentrantLock();

public int getMoney(int number) {
	lock.lock();
	try {
		if(number < 0) {//判断取款金额是否正确
			return -1;
		}else if(money < 0) {//判断账户里面的余额是否正确
			return -2;
		}else if(number > money) {//判断取款金额是否超过存折里面的金额
			return -3;
		}else {
			try {
				Thread.sleep(1000);//休眠1秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
		money-=number;//取钱之后存款的余额
		System.out.println("余额"+money);//输出余额
		return number;
	} finally {
        /**
	     *  这里一定要释放掉锁,避免出现线程死锁的情况
		 */
		lock.unlock();
	}
}

5、使用局部变量实现线程同步

使用ThreadLocal管理变量,每一个使用该变量的线程都会获取一个变量的副本,副本之间是相互独立的,所以每个线程只改变自己的副本内容,各个线程之间是互不干扰的。

ThreadLocal的常用方法:

  1. ThreadLocal :创建一个本地的变量
  2. get():返回当前副本变量的值
  3. set():设置当前副本变量的值
  4. initialValue() :返回当前线程副本的初始值

代码:

private int temp;
	
private int temp;

private static ThreadLocal<Integer> money = new ThreadLocal<Integer>() {

	@Override
	protected Integer initialValue() {
		return 500;
	}
	
};



public int getMoney(int number) {
	if(number < 0) {//判断取款金额是否正确
		return -1;
	}else if(money.get() < 0) {//判断账户里面的余额是否正确
		return -2;
	}else if(number > money.get()) {//判断取款金额是否超过存折里面的金额
		return -3;
	}else {
		try {
			Thread.sleep(1000);//休眠1秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
	
	
	temp = money.get();
	temp -=number;//取钱之后存款的余额
	money.set(temp);
	System.out.println("余额"+money.get());//输出余额
	return number;
}

结果:

余额100
余额100
取款400
取款400

 6、使用阻塞队列实现线程同步

现在我们使用java.util.concurrent包提供的方法来实现线程同步, LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。 

LinkedBlockingQueue 类常用方法 :

  1. LinkedBlockingQueue() : 创建一个自己定义大小容量的LinkedBlockingQueue
  2. put(E e):在队尾添加一个元素,如果队列满则阻塞
  3.  size() :队列中元素个数
  4.  take() :移除并返回队列头元素,如果队列为空则阻塞

代码(网上例子,简单明白):

package Thread.study.sync;

import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingSynchronizedThread {
	
	/**
	 * 定义应该阻塞对列
	 */
	private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
	
	private final static int size = 10;
	
	private int flag = 0;//0启动生产线程  1启动消费线程
	
	private class LinkBlockThread implements Runnable{

		@Override
		public void run() {
			int new_flag = flag++;
			System.out.println("启动线程 " + new_flag);
			if (new_flag==0) {
				for (int i = 0; i < size; i++) {
					int b = new Random().nextInt(255);
					System.out.println("生产的商品"+b+"号");
					try {
						queue.put(b);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("仓库中还有商品:"+queue.size()+"个");
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}else {
				for (int i = 0; i <size/2; i++) {
					try {
						int n = queue.take();
						System.out.println("销售出商品:"+n+"号");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("仓库中还有商品:"+queue.size()+"个");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
		
	}
	
	public static void main(String[] args) {
		BlockingSynchronizedThread bThread = new BlockingSynchronizedThread();
		Runnable runnable = bThread.new LinkBlockThread();
		Thread t1 = new Thread(runnable);
		Thread t2 = new Thread(runnable);
		t1.start();
		t2.start();
	}
	
	
}

结果:

启动线程 1
启动线程 0
生产的商品19号
仓库中还有商品:1个
销售出商品:19号
生产的商品139号
仓库中还有商品:1个
销售出商品:139号
生产的商品90号
仓库中还有商品:1个
销售出商品:90号
生产的商品153号
仓库中还有商品:1个
销售出商品:153号
生产的商品50号
仓库中还有商品:1个
销售出商品:50号
生产的商品17号
仓库中还有商品:0个
仓库中还有商品:1个
生产的商品90号
仓库中还有商品:2个
生产的商品51号
仓库中还有商品:3个
生产的商品30号
仓库中还有商品:4个
生产的商品224号
仓库中还有商品:5个

7、使用原子变量实现线程同步

什么是原子操作?

原子操作是不需要synchronized,是指不会被“线程调度机制”打断的操作,这种操作一旦开始不会有任何 context switch (换到另一个线程)。所以说就像操作变量,包含读取变量值、修改、保存和传输变量值等一系列的操作,都看成一个整体来操作的,直到结束,中途是不会有别线程来影响这个整体的操作。

举例一个原子操作,使用AtomicInteger:

  1. AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
  2. addAddGet(int dalta) : 以原子方式将给定值与当前值相加
  3. get() : 获取当前值

代码:

private AtomicInteger money = new AtomicInteger(500);
	
private Integer temp;

public int getMoney(int number) {
	if(number < 0) {//判断取款金额是否正确
		return -1;
	}else if(money.get() < 0) {//判断账户里面的余额是否正确
		return -2;
	}else if(number > money.get()) {//判断取款金额是否超过存折里面的金额
		return -3;
	}else {
		try {
			Thread.sleep(1000);//休眠1秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}
	
	
	temp = money.get();
	temp -=number;//取钱之后存款的余额
	money.set(temp);
	System.out.println("余额"+money.get());//输出余额
	return number;
}

结果:

余额100
余额100
取款400
取款400

从结果来看,俩条线程操作变量是相互独立,在每条线程操作变量值得时候是完整的,互不干扰的,所以输出结果是一至的。

"共享数据"的理解

synchronized保护的是共享数据,它的目的使同一对象产生的多线程,访问synchronized修饰的方法。

代码:

package Thread.study.join;

public class ThreadSharedDataTest {
	public static void main(String[] args) {
		Runnable runnable1 = new ThreadSharedData();
		//Runnable runnable2 = new ThreadSharedData();
		Thread t1 = new Thread(runnable1);
		Thread t2 = new Thread(runnable1);
		t1.start();
		t2.start();
	}
}
class ThreadSharedData implements Runnable {

	@Override
	public synchronized void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread()+":"+i);
		}
	}
		
}

这里如果放开注释,将t2用runable2生成新线程,测试的结果是每次输出的结果是不一样的。如果没有放开注释,每次的结果是一致的,总是先输出t1,在输出t2。

总结:文字表达不清楚的,只能自己的敲代码,多修改代码,尝试不同的结果,这样就能体会到文字所描述的含义。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值