文章目录
1、线程安全
1.1 一个简单的线程安全例子
1)下面的例子是,有一张银行卡,里面有1000的余额,两个人同时在取款机上取钱。
package thread;
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456", 1000);
DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
Thread myThread1 = new Thread(drawMoneyRunnable);
Thread myThread2 = new Thread(drawMoneyRunnable);
myThread1.start();
myThread2.start();
}
}
class DrawMoneyRunnable implements Runnable {
private Account account;
private double drawAmount;
public DrawMoneyRunnable(Account account, double drawAmount) {
super();
this.account = account;
this.drawAmount = drawAmount;
}
public void run() {
if (account.getBalance() >= drawAmount) { // 1
System.out.println("取钱成功, 取出钱数为:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("余额为:" + balance + "\n");
}else {
System.out.println("取钱失败!");
System.out.println("余额为:" + account.getBalance()+",不够你要取出的数目。");
}
}
}
class Account {
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
2)程序的输出结果:
也就是说,对于一张只有1000余额的银行卡,两个人一共可以取出1400,这显然是有问题的。
1.2 什么是线程安全
-
线程安全,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
-
“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。
-
如果两个线程同时操作对象中的实例变量,则会出现“非线程安全”,解决办法就是在方法前加上synchronized关键字即可。
2、synchronized——同步锁
- 如果不知道什么是锁,这里有:多线程基础篇
2.1 同步方法
1)什么是同步方法:
- 对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,访问此方法,以此类推…
2)将上面代码中的run()方法改为同步方法:
public synchronized void run() {
if (account.getBalance() >= drawAmount) { // 1
System.out.println("取钱成功, 取出钱数为:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("余额为:" + balance + "\n");
}else {
System.out.println("取钱失败!");
System.out.println("余额为:" + account.getBalance()+",不够你要取出的数目。");
}
}
- 输出结果如下:
3)synchronized同步方法与锁对象(重点)
-
synchronized本身没有锁的功能,但是他能获取对象锁,何为对象锁?Java的每个对象都有一个内置锁,简称对象锁。synchronized同步方法就是给这个方法加了一个对象锁,其锁对象为当前方法所在的对象自身,也就是哪个对象调用这个方法,就给这个方法加那个对象的锁。
-
思考这个问题:在一个类中,有两个同步方法a() 和 b()(方法前面有synchronized修饰),这两个方法没有任何关系。如果说现在有一个线程A正在访问其中一个方法a(),在访问的这段时间,又来了一个线程B,那么线程B能不能访问方法b()呢?
-
肯定是不行的。为什么呢,因为同步方法的锁是调用这个方法的对象的内置锁,两个方法在同一个类,肯定是同一个对象调用这两个方法,也就是同一个对象锁,既然是同一个对象锁,肯定是不可以的。
-
上面说的这个性质,其实也就是锁的互斥性。看下面代码:如果线程A先执行,则会进去死循环,线程B一直处于同步阻塞的状态,因为线程A一直没有释放锁资源;如果线程B先执行,则会输出这个"B进入test2方法",然后释放锁资源,这时候线程A才可以执行。
class MyThreadX implements Runnable{ @Override public void run() { test1(); test2(); } private synchronized void test2() { if(Thread.currentThread().getName().equals("B")){ System.out.println("B进入test2方法"); } } private synchronized void test1() { if(Thread.currentThread().getName().equals("A")){ while (true){} } } } public class Test12 { public static void main(String[] args) { MyThreadX mt=new MyThreadX(); Thread thread=new Thread(mt,"A"); Thread thread1=new Thread(mt,"B"); thread.start(); thread1.start(); } }
4)synchronized锁的可重入性(重点)
-
可重入锁就是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
class MyThreadX implements Runnable { @Override public void run() { test1(); } private synchronized void test2() { System.out.println(Thread.currentThread().getName() + "进入test2方法"); } private synchronized void test1() { if (Thread.currentThread().getName().equals("A")){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("A进入test1方法"); test2(); } } } public class Test12 { public static void main(String[] args) { MyThreadX mt = new MyThreadX(); Thread thread = new Thread(mt, "A"); thread.start(); } }
-
可重入锁也支持在父子类继承的环境中,说明当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。
5)同步方法不具有继承性(重点)
- 如果父类有一个带synchronized关键字的方法,子类继承并重写了这个方法。
但是同步不能继承,所以还是需要在子类方法中添加synchronized关键字。
6)synchronized同步方法的缺点
- 使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。
- 也就是说,同步方法锁定的范围太大了,这个时候,我们就可以考虑使用 同步代码块。
2.2 同步代码块
1)什么是同步代码块:
-
正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。
-
同步代码块就是个一段代码,加一个对象锁,和同步方法效果一样,只不过是范围更加小了。
synchronized (obj) { //... }
2)将上面的安全例子改成同步代码块的形式:
public void run() {
synchronized (account) {
if (account.getBalance() >= drawAmount) { // 1
System.out.println("取钱成功, 取出钱数为:" + drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("余额为:" + balance + "\n");
} else {
System.out.println("取钱失败!");
System.out.println("余额为:" + account.getBalance() + ",不够你要取出的数目。");
}
}
}
- 输出结果:
3)同步代码块的锁对象(重点):
- 同步代码后面跟着一个对象obj,
synchronized (obj)
,obj是哪个对象,就代表着,这个代码块使用的是那个对象的锁,选择哪一个对象作为锁是至关重要的。 - 如果使用
this
,就代表锁对象是执行这个代码块的对象的锁,其实也就是和同步方法的锁对象是一个道理。 - 一般情况下:都是选择此共享资源对象作为锁对象。如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心…)
4)同步代码块间的同步性(重点):
-
当一个对象访问synchronized(this)代码块时,其他线程对同一个对象中所有其他synchronized(this)代码块代码块的访问将被阻塞,这说明synchronized(this)代码块使用的“对象监视器”是一个。
也就是说和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。 -
另外通过上面的学习我们可以得出两个结论。
- 其他线程执行对象中synchronized同步方法(上一节我们介绍过,需要回顾的可以看上一节的文章)和synchronized(this)代码块时呈现一样的同步效果,只不过后者的作用范围跟小一些;
- 如果两个线程使用了同一个“对象监视器”,运行结果同步,否则不同步.
2.3 静态同步方法/代码块
-
synchronized关键字加到static静态方法和synchronized(class)代码块上都是是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。
// 共享资源 class Service { //静态方法,获取的Class锁 public static void printA() { synchronized (Service.class) { //这里要用类的class对象 try { System.out.println( "线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printA"); Thread.sleep(3000); System.out.println( "线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printA"); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized public static void printB() { System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printB"); System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printB"); } synchronized public void printC() { System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printC"); System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printC"); } } class ThreadA extends Thread { private Service service; public ThreadA(Service service) { super(); this.service = service; } @Override public void run() { Service.printA(); } } class ThreadB extends Thread { private Service service; public ThreadB(Service service) { super(); this.service = service; } @Override public void run() { Service.printB(); } } class ThreadC extends Thread { private Service service; public ThreadC(Service service) { super(); this.service = service; } @Override public void run() { service.printC(); } } public class Run { public static void main(String[] args) { Service service = new Service(); ThreadA a = new ThreadA(service); a.setName("A"); a.start(); ThreadB b = new ThreadB(service); b.setName("B"); b.start(); ThreadC c = new ThreadC(service); c.setName("C"); c.start(); } }
运行结果:
-
从运行结果可以看出:线程A,B和线程C持有的锁不一样,所以A和B运行同步,但是和C运行不同步。
-
静态同步synchronized方法与synchronized(class)代码块持有的锁一样,都是Class锁,Class类锁对所有的使用
Class.class
对象锁的方法或代码块起作用。synchronized关键字加到非static静态方法上持有的是对象锁,与Class类锁不会产生同步。
2.4 synchronized释放锁的时机
-
当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
-
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。不会由于异常导致出现死锁现象~
-
如果只是进入阻塞状态,不会释放锁,比如调用sleep方法。
3、Lock——显式锁
3.1 Lock锁概述
Lock锁,可以得到和 synchronized一样的效果,即实现原子性、有序性和可见性。
相较于synchronized,Lock锁可手动获取锁和释放锁、可中断的获取锁、超时获取锁。
Lock 是一个接口,两个直接实现类:
- ReentrantLock(重入锁)
- ReentrantReadWriteLock(读写锁)。
1)有什么办法方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题?
-
使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:
class X { // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系 private final Lock lock = new ReentrantLock(); public void m() { // 加锁 lock.lock(); try { // ... 需要进行线程安全同步的代码 } finally { // 释放Lock锁 lock.unlock(); } } }
2)给上面的线程安全问题加上Lock锁:
-
l.lock()方法进行上锁, l.unlock()方法进行解锁
class DrawMoneyRunnable implements Runnable { private Account account; private double drawAmount; private final Lock lock = new ReentrantLock(); public DrawMoneyRunnable(Account account, double drawAmount) { super(); this.account = account; this.drawAmount = drawAmount; } public void run() { lock.lock(); try { if (account.getBalance() >= drawAmount) { // 1 System.out.println("取钱成功, 取出钱数为:" + drawAmount); double balance = account.getBalance() - drawAmount; account.setBalance(balance); System.out.println("余额为:" + balance + "\n"); } else { System.out.println("取钱失败!"); System.out.println("余额为:" + account.getBalance() + ",不够你要取出的数目。"); } } finally { lock.unlock(); } } }
3.2 Lock锁 与 synchronized锁比较
1)两者的区别:
-
首先synchronized是java内置关键字,在jvm层面,Lock是个java接口,他有实现类;
-
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
-
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
-
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
-
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
-
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
4、wait()/notify()/notifyAll()线程通信
虽然这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。因此,理论上,任何Object对象都可以作为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通信。
1)三个方法的api
-
wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。
-
notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
-
notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。
package com.qqyumidi;
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456", 0);
Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700);
Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700);
drawMoneyThread.start();
depositeMoneyThread.start();
}
}
class DrawMoneyThread extends Thread {
private Account account;
private double amount;
public DrawMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i < 100; i++) {
account.draw(amount, i);
}
}
}
class DepositeMoneyThread extends Thread {
private Account account;
private double amount;
public DepositeMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i < 100; i++) {
account.deposite(amount, i);
}
}
}
class Account {
private String accountNo;
private double balance;
// 标识账户中是否已有存款
private boolean flag = false;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
/**
* 存钱
*
* @param depositeAmount
*/
public synchronized void deposite(double depositeAmount, int i) {
if (flag) {
// 账户中已有人存钱进去,此时当前线程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i);
wait();
// 1
System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 开始存钱
System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);
setBalance(balance + depositeAmount);
flag = true;
// 唤醒其他线程
notifyAll();
// 2
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-- 存钱 -- 执行完毕" + " -- i=" + i);
}
}
/**
* 取钱
*
* @param drawAmount
*/
public synchronized void draw(double drawAmount, int i) {
if (!flag) {
// 账户中还没人存钱进去,此时当前线程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " 执行了wait操作" + " -- i=" + i);
wait();
System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " 执行了wait操作" + " -- i=" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 开始取钱
System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount + " -- i=" + i);
setBalance(getBalance() - drawAmount);
flag = false;
// 唤醒其他线程
notifyAll();
System.out.println(Thread.currentThread().getName() + "-- 取钱 -- 执行完毕" + " -- i=" + i); // 3
}
}
}
2)要注意的点:
-
1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;
-
2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;
-
3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;
-
4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;
-
5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。
5、volatile关键字
- 修饰变量,保证变量的可见性,不保证原子性。
5.1 volatile关键字的可见性
1) 补个下面用到的知识点:
-
主存是公共空间,基本可以类比为虚拟机模型中的堆,对象创建好了都是在主存里,所有线程都可以访问(共享)。
-
工作内存(下文所说的本地内存)是线程的私有内存,只有本线程可以访问,如果线程要操作主存中的某个对象,必须从主存中拷贝到工作内存,在对工作内存中的副本进行操作,操作后再写入主存,而不能对主存的对象直接操作 。
2) volatile 修饰的成员变量在每次被线程访问时,都强迫从主存(共享内存)中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主存(共享内存)。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,这样也就保证了同步数据的可见性。
package thread.syn;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
System.out.println("已经赋值为false");
}
}
class RunThread extends Thread {
private boolean isRunning = true;
int m;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入run了");
while (isRunning == true) {
int a = 2;
int b = 3;
int c = a + b;
m = c;
}
System.out.println(m);
System.out.println("线程被停止了!");
}
}
运行结果:死循环
-
RunThread类中的isRunning变量没有加上volatile关键字时,运行以上代码会出现死循环,这是因为isRunning变量虽然被修改但是没有被写到主存中,这也就导致该线程在本地内存中的值一直为true,这样就导致了死循环的产生。
-
解决办法也很简单:isRunning变量前加上volatile关键字即可。
3)注意下面这个问题:
假如你把while循环代码里加上任意一个输出语句或者sleep方法你会发现死循环也会停止,不管isRunning变量是否被加上了上volatile关键字。
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
System.out.println(m);
}
//或者:
while (isRunning == true) {
int a=2;
int b=3;
int c=a+b;
m=c;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
- 这是为什么?
JVM会尽力保证内存的可见性,即便这个变量没有加同步关键字。换句话说,只要CPU有时间,JVM会尽力去保证变量值的更新。这种与volatile关键字的不同在于,volatile关键字会强制的保证线程的可见性。而不加这个关键字,JVM也会尽力去保证可见性,但是如果CPU一直有其他的事情在处理,它也没办法。最开始的代码,一直处于死循环中,CPU处于一直占用的状态,这个时候CPU没有时间,JVM也不能强制要求CPU分点时间去取最新的变量值。而加了输出或者sleep语句之后,CPU就有可能有时间去保证内存的可见性,于是while循环可以被终止。
5.2 volatile关键字能保证原子性吗?
volatile是无法保证原子性的,要保证数据的原子性还是要使用synchronized关键字。
5.3 synchronized关键字和volatile关键字比较
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用synchronized关键字还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
- volatile关键字用于解决变量在多个线程之间的可见性,而ynchronized关键字解决的是多个线程之间访问资源的同步性。
文章参考地址,这部分内容基本上都是出自这里:https://blog.csdn.net/qq_34337272/article/details/79680771