1. 线程安全
1.1 线程安全概述
如果有多个线程在同时运行, 这些线程有可能会同时运行一段代码, 程序每次运行结果和单线程运行的结果是一样的, 而且其他的变量的值也和预期相同, 这就是一个安全的线程.
1.2 案例演示
public class Ticket implements Runnable {
int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
if (ticket <= 0) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("售票员" + Thread.currentThread().getName() + "售出一张票,剩余:" + ticket);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口小姐姐A");
Thread t2 = new Thread(ticket, "窗口小姐姐B");
Thread t3 = new Thread(ticket, "窗口小姐姐C");
t1.start();
t2.start();
t3.start();
}
}
1.3 问题发现及解决
通过运行发现, 上面的程序出现了问题, 票出现了重复的票
其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全
本质原因:
有多个线程在同时访问一个资源,如果一个线程在取值的过程中,时间片又被其他线程抢走了,临界资源问题就产生了。
解决办法:
一个线程在访问临界资源的时候,如果给这个资源“上一把锁”,这个时候如果其他线程也要访问这个资源,就得在“锁”外面等待
1.3.1 同步方法
同步方法:
在方法声明上加上synchronized
public synchronized void method(){
可能会产生线程安全问题的代码
}
public class Ticket implements Runnable {
int ticket = 10;
@Override
public void run() {
while (ticket > 0) {
sellTickets();
}
}
public synchronized void sellTickets() {
if (ticket <= 0) {
return;
}
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("售票员" + Thread.currentThread().getName() + "售出一张票,剩余为" + ticket);
}
}
1.3.2 同步代码块
同步代码块: 在代码块声明上 加上synchronized
synchronized (锁对象) {
可能会产生线程安全问题的代码
}
public class Ticket implements Runnable {
int ticket = 10;
String str = new String();
@Override
public void run() {
while (ticket > 0) {
synchronized (str) {
if (ticket <= 0) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println("售票员" + Thread.currentThread().getName() + "售出一张票,剩余:" + ticket);
}
}
}
}
1.3.3 总结
a.程序走到代码段中,就用锁来锁住了临界资源,这个时候,其他线程不能执行代码段中的代码,只能在锁外边等待
b.执行完代码段中的这段代码,会自动解锁。然后剩下的其他线程开始争抢cpu时间片
c.一定要保证不同的线程看到的是同一把锁,否则同步代码块没有意义。
1.4 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉
1.5 Lock
除了使用Synchronized来锁定临界资源, 我们还可以使用Lock接口
代码格式:
void lock();
void unlock();
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。通过使用ReentrantLock这个类来进行锁的操作,它实现了Lock接口,使用ReentrantLock可以显式地加锁、释放锁
我们使用Lock接口,以及其中的lock()方法和unlock()方法替代synchronized,对卖票案例中Ticket类进行如下代码修改:
public class ReentrantLockDemo01 implements Runnable {
int count = 100;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (count > 0) {
lock.lock();
if (count <= 0) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("售票员" + Thread.currentThread().getName() + "售出一张票,剩余为" + count);
lock.unlock();
}
}
}
public class TestLock {
public static void main(String[] args) {
ReentrantLockDemo01 r=new ReentrantLockDemo01();
Thread t0 = new Thread(r, "喜羊羊");
Thread t1 = new Thread(r, "沸羊羊");
Thread t2 = new Thread(r, "灰太狼");
Thread t3 = new Thread(r, "小灰灰");
t0.start();
t1.start();
t2.start();
t3.start();
}
}
1.6 等待唤醒机制
线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
常用方法:
wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
public class Demo1 {
public static void main(String[] args) {
Test1 thread = new Test1();
Thread ta = new Thread(thread);
ta.setName("线程AAAA");
ta.start();
Thread tb = new Thread(thread);
tb.setName("线程BBBB");
tb.start();
}
}
class Test1 implements Runnable {
@Override
synchronized public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "===" + i);
if (i == 2) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i == 1) {
notify();
}
if (i == 4) {
notify();
}
}
}
}