线程为什么要使用同步?
当一个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的使用场合:
- 虽然俩个都是实现线程同步的,但是他们对程序来说开销大,所以最好的情况就是都不使用,使用java.util.concurrent包提供的机制, 能够帮助用户处理所有与锁相关的代码。
- synchronized能够简化代码。
- 如果需要更高级的功能,就用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的常用方法:
- ThreadLocal :创建一个本地的变量
- get():返回当前副本变量的值
- set():设置当前副本变量的值
- 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 类常用方法 :
- LinkedBlockingQueue() : 创建一个自己定义大小容量的LinkedBlockingQueue
- put(E e):在队尾添加一个元素,如果队列满则阻塞
- size() :队列中元素个数
- 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:
- AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
- addAddGet(int dalta) : 以原子方式将给定值与当前值相加
- 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。
总结:文字表达不清楚的,只能自己的敲代码,多修改代码,尝试不同的结果,这样就能体会到文字所描述的含义。