相关概念
线程安不安全: 多线程程序中,多个线程共同访问共享资源 ( 共有的资源 ) 时,由于线程的调度机制是抢占式调度,可能会发生多个线程在执行时,同时操作共享资源,导致程序执行结果与预期不一致的现象。
线程安全:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
案例
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个 (本场电影只能卖100张票)。 我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票) 需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟
模拟票:
public class Ticket implements Runnable{
private int ticket=50;
/**
* 卖票操作
*/
@Override
public void run() {
//每个窗口的卖票操作
//窗口永久开启
while (true) {
if (ticket > 0) {//有票可以继续卖
//出票操作
//使用sleep模拟下出票过程耗费的实际
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket--+"号票");
} else {
System.out.println(Thread.currentThread().getName() + "票已卖完!");
break;//售完关闭窗口
}
}
}
}
测试类:
public class TicketDemo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个线程作为三个售票窗口
Thread thread1 = new Thread(ticket, "售票员1号");
Thread thread2 = new Thread(ticket, "售票员2号");
Thread thread3 = new Thread(ticket, "售票员3号");
//同时售票
thread1.start();
thread2.start();
thread3.start();
}
}
结果:
结果的问题:
出现两个甚至三个售票员售同一张票的情况;
还有出先-1这样非法票的情况;
这样票数不同步的问题就是一种线程不安全问题,原因是三个售票员同时访问了Ticket中的全局变量ticket,并且执行了写的操作。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码 去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
同步机制
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
同步操作的实现方式有以下三种:
- 同步代码块。
- 同步方法。
- 锁机制。
同步代码块
同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
格式:
synchronized(同步锁){
需要同步操作的代码
}
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,在任何时候,最多允许一个线程拥有同步锁,只有拿到该锁的线程才能访问该对象,进入代码块,其他的线程只能在外等着 (BLOCKED)。
锁对象的类型:
a.this对象
b.class对象,如ticket.class //给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁
c.任意一个对象// Object lock = new Object(); 用这个比较容易理解
this作锁的示例
传入this关键字指代的是当前对象即Ticket的实例,其他试图访问该对象的线程将被阻塞。
public class Ticket implements Runnable {
private int ticket = 50;
/**
* 卖票操作
*/
@Override
public void run() {
//每个窗口的卖票操作
//窗口永久开启
while (true) {
synchronized (this) {
if (ticket > 0) {//有票可以继续卖
//出票操作
//使用sleep模拟下出票过程耗费的实际
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket-- + "号票");
} else {
System.out.println(Thread.currentThread().getName() + "票已卖完!");
break;//售完关闭窗口
}
}
}
}
}
结果正常:
总结:给某个对象加锁:
1)当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。
public void method3(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}
2)当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
class Test implements Runnable
{
Object lock = new Object(); // 随便的一个对象锁
public void method()
{
synchronized(lock) {
// todo 同步代码块
}
}
public void run() {
}
}
同步方法:
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
锁对象:
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
锁对象是谁调用这个方法就是谁,隐含锁对象就是this
示例:
public class TicketRunnable2 implements Runnable {
private Integer ticket = 50;
@Override
public void run() {
//窗口永久开启
while (true) {
if (!sellTicket())
break;
}
}
/**
* 锁对象是谁调用这个方法就是谁,隐含锁对象就是this
* @return
*/
private synchronized boolean sellTicket() {
if (ticket > 0) {//有票可以继续卖
//出票操作
//使用sleep模拟下出票过程耗费的实际
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket-- + "号票");
return true;
} else {
System.out.println(Thread.currentThread().getName() + "票已卖完!");
return false;
}
}
}
Lock锁:
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
使用:
public class LockTicket implements Runnable {
private int ticket = 50;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (ticket > 0) {
//出票操作
//使用sleep模拟下出票过程耗费的实际
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在售卖" + ticket-- + "号票");
} else {
System.out.println(Thread.currentThread().getName() + "票已卖完!");
break;
}
lock.unlock();
}
}
}
线程的状态:
线程状态 导致状态发生条件
- NEW(新建) :线程刚被创建,但是并未启动。还没调用start方法。 1.
- Runnable(可 运行):线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。 对应于操作系统中的线程的就绪和运行状态。
- Blocked(锁阻 塞):当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
- Waiting(无限 等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
- Timed Waiting(计时等待) :同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。
- Teminated(被 终止) :因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
线程生命周期的线程图:
注意:被唤醒的线程并不能立即执行,而是要重新去竞争锁,得到锁后才能继续执行。得不到锁就会处于BLOCKED阻塞状态。
上述的生命周期中,创建、执行、阻塞与结束都比较好理解,这里主要通过一个联系学习下Waitting
waiting的案例。
public class WaitingTestSyschronized {
private static Object object = new Object();
public static void main(String[] args) {
//匿名内部类方式创建线程
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (object) {
try {
System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用wait方法,进入waiting状态,释放锁对象");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "从waiting状态中醒来,获得锁对象继续执行!");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) { //每三秒唤醒一次
synchronized (object) {
try {
// Thread.sleep(3000);wait和sleep的区别在下面有讲
object.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用notify方法,释放锁对象");
object.notify();//唤醒
}
}
}
}).start();
}
}
结果:
下面用Lock锁示例了等待和唤醒的方式,lock锁的方式不能用wait和notify,否则会报IllegalMonitorStateException错误。
因为调用wait的对象必须是唯一的,必须被synchronized加锁。所以要用lock自己特定的方式来加锁解锁,以及实现等待唤醒。
ReentrantLock.lock可以对应理解成synchronized刚进入代码块获取到锁
ReentrantLock.unlock可以对应理解成synchronized代码块结束释放锁
Condition condition = reentrantLock.newCondition()
condition.await 可以理解成 Object.wait 方法的封装
condition.signal 可以理解成 Object.notify 方法的封装
public class WaitingTest {
//Lock锁
private static Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main(String[] args) {
//匿名内部类实现线程的创建
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
lock.lock();//加锁
try {
System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用await方法,进入waiting状态,释放锁对象");
condition.await();
// condition.await(5000,TimeUnit.MILLI_SCALE); //计时等待, 5秒时间到,自动醒来
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "从waiting状态中醒来,获得锁对象继续执行!");
lock.unlock();//解锁
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) { //每三秒唤醒一次
lock.lock();
try {
// Thread.sleep(3000);
condition.await(3000, TimeUnit.MILLI_SCALE);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获得锁对象后,调用signal方法,释放锁对象");
condition.signal();//唤醒
lock.unlock();//解锁
}
}
}).start();
}
}
结果:
wait和sleep的区别
1、sleep是线程中的方法,但是wait是Object中的方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字,只有synchronized加锁的对象才能调用wait和notify。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是没有指定时间的wait需要被其他线程唤醒,指定时间自动唤醒的则不需要。
线程间的通信
为什么需要线程间的通信:多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
实现线程通信的方式:多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就 是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效 的利用资源。而这种手段即—— 等待唤醒机制。
上述的wait与notify,以及await和signal就是一种等待唤醒机制,类似操作系统了的pv操作。
注意: 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。
使用线程的时候就去创建一个线程,这样实现起来非常简便,但并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束的情况下,频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。
就需要使用线程池来实现线程的复用,提高效率。有关线程池可以看这篇文章。
线程池使用