多线程是我们在日常开发中十分重要的环节,这个技术点可以实现程序功能同时执行,同时完成多个任务。但是如果多线程技术使用不当,或者逻辑不合理,就会造成线程安全问题,这个问题不出现还好,一出现就是大问题,所以必须严加防范。
这里使用实现购票功能案例来展示线程安全问题
public class TestRunnable3 implements Runnable{
private int ticket = 10;
@Override
public void run() {
while (true){
if(ticket<=0){
break;
}
//因为CPU执行的太快,导致分配不均匀,这里加上一个模拟延时的方法
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+ +ticket-- + "张票");
}
}
public static void main(String[] args) {
TestRunnable3 testRunnable3 = new TestRunnable3();
new Thread(testRunnable3,"小明").start();
new Thread(testRunnable3,"小红").start();
new Thread(testRunnable3,"小兰").start();
}
}
运行结果: 通过运行可以发现,多个人同时抢到了同一张票,这显然是不合理的,并且这种结果还会实际应用中造成非常严重的后果,必须要避免此类状况的发生。
这种线程安全问题是因为同一个共享资源同时被多个线程所使用导致的。想要避免这种状况就需要将这些这些共享的资源锁起来,让这些共享资源在同一时间只能由一个线程使用,只有之前的线程使用完下一个线程才能继续使用资源。
多线程中的锁可以解决绝大多数因为多个线程互相争夺资源导致的线程不安全问题。
其中的原理就像是将线程归拢成队列的形式,各个线程需要排队去使用这些资源。并且将这些资源加上一个锁,当线程使用资源时,这个锁会阻拦其他线程使用该资源。只有正在使用资源的线程执行完毕,下一个线程才可以使用该资源。
一句话总结锁的作用:一群人排队做核算,前面的做完了,下一个才可以做。
加锁的形式有两种:
隐式加锁:隐式的加锁的方式有两种,同步代码块以及同步方法。使用隐式加锁的方式无法看到加锁和释放锁的过程。
显式加锁:在JDK5以后,Java 提供了一个新的锁对象Lock,可以更加更清晰的表达如何加锁和释放锁的整个过程。
Demo代码示例:
public class TestRunnable3 implements Runnable{
// 定义10张票
private int tickets = 10;
//创建锁对象
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
public static void main(String[] args) {
TestRunnable3 testRunnable3 = new TestRunnable3();
new Thread(testRunnable3,"小明").start();
new Thread(testRunnable3,"小红").start();
new Thread(testRunnable3,"小兰").start();
}
}
运行结果:
同步方法
public class TestRunnable3 implements Runnable{
// 定义10张票
private int tickets = 10;
@Override
public void run() {
while (true) {
synchronization();
}
}
// 定义同步方法
public synchronized void synchronization(){
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
public static void main(String[] args) {
TestRunnable3 testRunnable3 = new TestRunnable3();
new Thread(testRunnable3,"小明").start();
new Thread(testRunnable3,"小红").start();
new Thread(testRunnable3,"小兰").start();
}
}
运行结果:
线程的安全问题虽然可以使用同步代码块和同步方法来解决。但是并不能看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Demo代码示例:
public class TestRunnable3 implements Runnable{
// 定义10张票
private int tickets = 10;
// 定义锁对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
try {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
} finally {
// 释放锁
lock.unlock();
}
}
}
public static void main(String[] args) {
TestRunnable3 testRunnable3 = new TestRunnable3();
new Thread(testRunnable3,"小明").start();
new Thread(testRunnable3,"小红").start();
new Thread(testRunnable3,"小兰").start();
}
}
运行方法:
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象。若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
减少死锁的办法:
1. 专门的算法、原则。
2. 尽量减少同步资源的定义。
3. 尽量避免嵌套同步。