Java线程的安全问题
当多个线程同时访问同一资源(变量,文件,记录),如果只有读操作,则不会有线程安全问题,如果有读和写操作,则会产生线程安全问题,必须保证共享数据同一时刻只能有同一个线程操作。Java采取的办法是synchronized同步代码块或同步方法。同步代码块或同步方法解决了线程安全问题,但是操作共享数据时,线程时串行执行的,意味着效率较低。
1.多线程安全问题
经典卖票案例:
两个线程一块卖票,没有加同步代码块,程序运行结果不正确,存在超卖重卖
public class Ticket {
public static void main(String[] args) {
TicketTask t1 = new TicketTask();
TicketTask t2 = new TicketTask();
t1.start();
t2.start();
}
static class TicketTask extends Thread {
static int ticket = 200;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +" " + ticket);
ticket--;
}
else {
break;
}
}
}
}
}
2 同步代码块和同步方法解决线程安全问题
2.1 同步代码块
需要被同步的代码,即为操作共享数据的代码。共享数据,即为多个线程都需要操作的数据。同步监视器可以由任何类的对象担任,但是多个线程必须共用同一个同步监视器。
同步代码块的语法
synchronized (同步监视器/锁) {
//需要被同步的代码
}
加入同步代码块,程序运行结果正确,当有线程操作共享数据,其他线程需要等待。lock作为同步监视器,锁住代码块中的操作,谁获得同步监视器,谁运行同步代码块中的代码。
public class Ticket {
public static void main(String[] args) {
TicketTask t1 = new TicketTask();
TicketTask t2 = new TicketTask();
t1.start();
t2.start();
}
static class TicketTask extends Thread {
static int ticket = 200;
static final Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +" " + ticket);
ticket--;
}
else {
break;
}
}
}
}
}
}
2.2 同步方法
如果需要同步执行的代码恰好在一个方法中,可以使用同步方法保证线程安全,在方法声明上使用 synchronized
关键字,此时锁为对象实例(this
)。同一时刻,只有一个线程能够执行该实例的方法。
public synchronized void test() {
}
3 同步代码块和同步方法的使用
3.1 Runnable创建线程时使用同步代码块
使用synchronized使得实例方法加锁变为同步方法,因为Runnable对象只有一个,所以锁可以直接使用当前调用者this
public class TestRunnable {
public static void main(String[] args) {
Target target = new Target();
for (int i = 0; i < 10; i++) {
new Thread(target, "T"+i).start();
}
}
}
class Target implements Runnable {
private Integer i = 1000;
@Override
public void run() {
while (true) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
if (i > 0) {
i--;
System.out.println(Thread.currentThread().getName() + "->" + i);
} else {
break;
}
}
}
}
}
3.2 Thread类创建线程时,使用同步代码块
Thread类创建线程时,使用同步代码块加锁,因为Thread对象是多个,所以需要静态的监视器对象object,如果还用this就出现了多个锁
public class TestThread {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new MyThread("T"+i).start();
}
}
}
class MyThread extends Thread {
private static Integer i = 1000;
//同步监视器,锁
private static Object object = new Object();
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while(true) {
synchronized(object) {
if (i > 0) {
i--;
System.out.println(Thread.currentThread().getName() +"->" + i);
} else {
break;
}
}
}
}
}
3.3 Runnable创建线程时,使用同步方法加锁
public class TestRunnable {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
private int num = 1000;
@Override
public void run() {
while (true) {
this.show();
}
}
public synchronized void show() {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (num > 0) {
num--;
System.out.println(Thread.currentThread().getName() + "->" + num);
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable, "T" + i).start();
}
}
}
3.4 Thread创建线程时,使用同步方法加锁
public class TestThread {
public static void main(String[] args) {
TicketTest ticketTest = new TicketTest();
for (int i = 0; i < 10; i++) {
new Thread("T"+i) {
@Override
public void run() {
while (true) {
ticketTest.test();
}
}
}.start();
}
}
static class TicketTest {
private int num = 1000;
private synchronized void test() {
if (num > 0) {
num--;
System.out.println(Thread.currentThread().getName() + "->" + num);
}
}
}
}
3.5 静态方法加锁和使用.class对象做锁
在静态方法上使用 synchronized
,锁住的是类的.class对象,每个类的class对象只有一个,所以同时只能有一个线程进入方法。
public static synchronized void staticMethod() {
// 方法体
}
同步块上使用.class对象做锁,因为每个类.class对象只有一个,故也能用于保证线程安全
synchronized (A.class) {
}
4 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
写程序时,要避免出现死锁。
示例代码1
死锁原因的分析:这个例子是个比较明显的死锁,线程t1
,t2
几乎同时启动,在一秒钟的等待时间里,t1
获得了锁lock1
,t2
获得了锁lock2
,一秒钟后t1
又想去获得lock2
,但是现在lock2
被t2
持有,需要一直等直到t2
释放lock2
,与此同时,t2
也想去获得lock1
,但是lock1
现在被t1
持有,需要一直等待,直到t1
释放lock1
,两个线程都在争抢在对方持有的锁,且都在等待对方先释放各自持有的锁,不然就一直等待,线程都一直处在阻塞状态无法继续运行,造成死锁。
public class TestDeadLock {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (lock2) {
System.out.println(lock1);
System.out.println(lock2);
}
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
synchronized (lock2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (lock1) {
System.out.println(lock1);
System.out.println(lock2);
}
}
}
};
t1.start();
t2.start();
}
}
示例代码2
这是一个不是非常明显的死锁的例子,线程thread1
和thread2
几乎同时开始执行,thread1
执行a.fun(b)
时,由于A
类的fun
方法是个同步方法,故锁是当前调用者this
对象,即a
,调用fun
方法,thread1
便持有了锁a
,与此同时,thread2
同理的持有了锁b
,这些都在一秒钟前完成了,1秒钟后,thread1
执行b
的last
同步方法,同理需要先获得锁b
,但是锁b
目前被thread2
持有,同时thread2
也开始执行a
的last
方法,需要先持有锁a
,但是锁a
被thread1
持有,双方都在等待对方先释放自己需要的锁,否则就一直阻塞无法继续运行,造成死锁。
public class TestDeadLock2 {
public static void main(String[] args) {
A a = new A();
B b = new B();
Thread thread1 = new Thread() {
@Override
public void run() {
a.fun(b);
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
b.fun(a);
}
};
thread1.start();
thread2.start();
}
public static class A extends Thread {
public synchronized void fun(B b) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
b.last();
}
public synchronized void last() {
}
}
public static class B extends Thread {
public synchronized void fun(A a) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
a.last();
}
public synchronized void last() {
}
}
}