- 学过操作系统或数据库的小伙伴都知道,当有多个线程同时对一个资源操作时,若没有保护措施就会不安全。
- 例如数据库中有一个值,现在有两个线程,一个读取,一个修改,若读取在前,修改在后,则读取到的是一个旧的值。
并发同步与线程的安全性
- 并发是指多个线程同时操作同一个资源,若控制不当我们就称为线程不安全。
- 下面这个代码就可以表述什么是线程不安全
public class Tickets {
public static void main(String[] args) {
Train train = new Train();
User user1 = new User(train);
User user2 = new User(train);
User user3 = new User(train);
User user4 = new User(train);
user1.start();
user2.start();
user3.start();
user4.start();
}
}
class Train{
//定义10张火车票
private int tickets = 10;
public void setTickets(int tickets) {
this.tickets = tickets;
}
public int getTickets() {
return tickets;
}
}
class User extends Thread{
Train train;
public User(Train train) {
this.train = train;
}
@Override
public void run() {
//模拟抢五次票
for(int i=0;i<5;i++) {
if (train.getTickets() > 0) {
//模拟网络延迟一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
train.setTickets(train.getTickets() - 1);
System.out.println(Thread.currentThread().getName()+"--->"+"抢到票了"+"还剩"+train.getTickets()+"张");
}
}
}
}
Thread-0--->抢到票了还剩9张
Thread-3--->抢到票了还剩8张
Thread-2--->抢到票了还剩9张
Thread-1--->抢到票了还剩9张
Thread-1--->抢到票了还剩7张
Thread-2--->抢到票了还剩6张
Thread-3--->抢到票了还剩5张
Thread-0--->抢到票了还剩4张
Thread-0--->抢到票了还剩3张
Thread-3--->抢到票了还剩2张
Thread-2--->抢到票了还剩1张
Thread-1--->抢到票了还剩0张
Thread-0--->抢到票了还剩-1张
Thread-3--->抢到票了还剩-1张
Thread-2--->抢到票了还剩-2张
- 我们看到本来应该只有10张票,因此最多就输出10次抢到票了,但结果显然不是这样的。出现这种结果就是线程不安全,因为这四个人可以同时访问并修改火车票,假如就剩下最后一张火车票,四个线程都走到了判断语句,并且在还没有修改的时候,那么四个线程都会抢到一张票,因此才会出现负数的问题
Synchronized保证java线程安全性
- 我们上面描述了线程的安全性,当有多个线程同时操作同一个资源时,这线程就是不安全的,会出现很多问题。
- 因此我们可以给资源加上一个锁,就像厕所一样,当有人进入厕所时会把门锁上,当操作完出去的时候再把门打开,这期间别人是进不去的。
- java就提供了这样一个锁Synchronized,分为方法和块。我们拿上边的抢票例子来举例。
- Synchronized方法
public synchronized void run() {
}
public void run() {
//参数是锁的资源,{}内是范围
synchronized (train) {
}
}
- 这里我们要明白,锁的不是方法和一块代码,而是资源,上边的方法锁其实是当访问这个方法时我们给这个线程对象加上了锁,后边的锁是给train这个对象加上了锁。
- 那么我们就可以猜想一下,抢购车票的那个例子中,加上Synchronized方法和Synchronized块可以解决之前的问题吗,答案是Synchronized块可以,Synchronized方法不行。当加入Synchronized块时,我们锁了train这个对象,当其他线程想要访问这个train对象时必须等到我们操作完释放锁后才能访问。所有最后的结果是正确的线程安全,当加入Synchronized方法时,我们其实锁的是当前线程,就是自己锁自己,别人又没有访问你自己,这个锁加了等于没加。
- 注:加上锁势必会带来性能的下降,因为你将大家共同操作的资源锁住了,以前是大家同时一起操作资源,现在是一个一个来,自然会带来性能的下降,因此我们再使用Synchronized的时候一定要控制好范围,既要让线程安全,又要尽量小的范围使用。
死锁的产生与解决
- 现在考虑一种情况,两个线程都需要AB资源,那么当AB资源加上锁后,第一个线程占有A资源,第二个线程占有B资源,那么第一个线程由于没有B资源处于等待,第二个线程由于没有A资源也处于等待,这样就会产生死锁,两个线程都会无限等待下去。
public class TestDeallock {
public static void main(String[] args) {
A a = new A();
B b = new B();
User1 user1 = new User1(a,b);
User2 user2 = new User2(a,b);
user1.start();
user2.start();
}
}
class A{
}
class B{
}
//先获取A再获取B
class User1 extends Thread{
A a;
B b;
public User1(A a, B b) {
this.a = a;
this.b = b;
}
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "获取A资源");
//延迟一秒再去获取下一个资源
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "获取B资源");
}
}
}
}
//这个类先获取B资源再获取A
class User2 extends Thread{
A a;
B b;
public User2(A a, B b) {
this.a = a;
this.b = b;
}
public void run() {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "获取B资源");
//延迟一秒再去获取下一个资源
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "获取A资源");
}
}
}
}
E:\Java\jdk1.8.0_171\bin\java.exe "-javaagent:E:\idea\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=65147:E:\idea\IntelliJ IDEA 2019.1.3\bin" -Dfile.encoding=UTF-8 -classpath E:\Java\jdk1.8.0_171\jre\lib\charsets.jar;E:\Java\jdk1.8.0_171\jre\lib\deploy.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\access-bridge-64.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\cldrdata.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\dnsns.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\jaccess.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\jfxrt.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\localedata.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\nashorn.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\sunec.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\sunjce_provider.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\sunmscapi.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\sunpkcs11.jar;E:\Java\jdk1.8.0_171\jre\lib\ext\zipfs.jar;E:\Java\jdk1.8.0_171\jre\lib\javaws.jar;E:\Java\jdk1.8.0_171\jre\lib\jce.jar;E:\Java\jdk1.8.0_171\jre\lib\jfr.jar;E:\Java\jdk1.8.0_171\jre\lib\jfxswt.jar;E:\Java\jdk1.8.0_171\jre\lib\jsse.jar;E:\Java\jdk1.8.0_171\jre\lib\management-agent.jar;E:\Java\jdk1.8.0_171\jre\lib\plugin.jar;E:\Java\jdk1.8.0_171\jre\lib\resources.jar;E:\Java\jdk1.8.0_171\jre\lib\rt.jar;E:\java多线程\out\production\java多线程 TestDeallock
Thread-0获取A资源
Thread-1获取B资源
- 这里程序并没有结束,若不人为关闭,它会一直这样。
- 这里产生死锁的原因就是上边讲的,第一个线程拥有A,但没有B无法继续执行,因此不会释放A的锁,第二个线程用于B,但没有A无法执行,也不会释放B的锁。
- 解决的方法有很多,最简单的就是不要锁套锁,就是线程使用一个资源后就释放这个的锁。
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "获取A资源");
//延迟一秒再去获取下一个资源
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "获取B资源");
}
}
- 第二个线程同理
- 死锁会产生的原因是线程与资源形成了一个闭环就是第一个资源用于A的锁,并在等待第二个线程释放B的锁,第二个资源用有B的锁,并在等待第一个线程释放A的锁。