线程安全问题及解决办法
JDK5.0之前就已经有了两种解决线程安全的问题:
- 同步代码块
- 同步方法
JDK5.0后新增了一种解决方案:
- 同步锁
同步的方法虽然能解决线程安全问题,但它带来的坏处就是降低了效率。所以,我们应该尽量减少需要同步的代码块,必不可少的地方才去用同步方法。
我们首先分别用继承Thread以及实现Runnable接口的方法,写出一个含线程安全的程序。
模拟业务:多窗口卖票(使用多线程模拟多个窗口),共有篇数100张。
继承Thread类方案:
// 继承Thread类
class Window1 extends Thread
{
private static int ticket = 100;
@Override
public void run ()
{
while (true) {
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":票号" + ticket--);
} else {
break;
}
}
}
}
public class Window1Test {
public static void main(String[] args) {
Window1 window11 = new Window1();
Window1 window12 = new Window1();
Window1 window13 = new Window1();
window11.setName("窗口1");
window12.setName("窗口2");
window13.setName("窗口3");
window11.start();
window12.start();
window13.start();
}
}
实现Runnable接口的方法
// 实现Runnable接口方法
class Window2 implements Runnable
{
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
} else {
break;
}
}
}
}
public class Window2Test {
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread thread1 = new Thread(window2);
Thread thread2 = new Thread(window2);
Thread thread3 = new Thread(window2);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
同步代码块
语法:
synchronized (同步监视器) {
// 需要同步的代码
}
同步监视器(锁)可以是任何类型的对象,但是多个线程要使用同一个同步监视器,否则依旧不是线程安全的。
我们首先用同步代码块的方式来改进上述的继承Thread版卖票程序:
public void run ()
{
while (true) {
synchronized (Window1.class) {
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":票号" + ticket--);
} else {
break;
}
}
}
}
修改实现Runnable接口的卖票程序方法和上述一样
public void run() {
while (true) {
synchronized (Window2.class) {
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
} else {
break;
}
}
}
}
同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
注意:同步方法虽然不能手动设置同步监视器,但它确实是存在的。实例方法的同步监视器为this(不一定唯一),而静态方法的同步监视器为
(唯一)当前类.class
使用同步方法修改Thread版卖票程序
// 继承Thread类
class Window1 extends Thread
{
private static int ticket = 100;
@Override
public void run ()
{
while (ticket > 0) {
ticketSale();
}
}
private static synchronized void ticketSale ()
{
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
}
}
}
使用同步方法修改Runnable版卖票程序
// 实现Runnable接口方法
class Window2 implements Runnable
{
private static int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
ticketSale();
}
}
private synchronized void ticketSale ()
{
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
}
}
}
同步锁
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class Window3 implements Callable
{
private static int ticket = 100;
private static final ReentrantLock lock = new ReentrantLock();
@Override
public Object call() {
while (true) {
lock.lock();
try {
if (ticket > 0) {
// 模拟卖票业务,每次卖票耗时0.1s
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票号" + ticket--);
} else {
break;
}
} finally {
lock.unlock();
}
}
return null;
}
}
public class Window3Test {
public static void main(String[] args) {
FutureTask[] tasks = new FutureTask[3];
Callable callable = new Window3();
for (int i = 0; i < 3; i++)
{
tasks[i] = new FutureTask(callable);
Thread t = new Thread(tasks[i]);
t.start();
}
}
}
synchronized 与 Lock的异同?
相同:
- 二者都可以解决线程安全问题
不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
更好的扩展性(提供更多的子类)
在实际开发选择中,到底应该使用哪种解决方案?
- 通常情况下,优先选择使用同步锁;其次是同步代码块;如果整个方法都是需要同步的代码,就选择同步方法。