- 一 线程安全
- 二 互斥锁之单例模式之懒汉式的线程安全问题
- 三 线程的死锁问题
- 四 线程通信
在之前的博客 java提升2相关章节 介绍了关于多线程的基础知识,可以翻阅查看,本章节深入了解一下多线程的应用。
一. 线程安全
对于线程安全问题,首先举个例子:
//窗口售票问题
class MyThreadDemo implements Runnable {
int ticket = 100;
@Override
public void run() {
while (true){
if (ticket>0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
}else{
break;
}
}
}
}
public class ThreadTest {
public static void main(String[] args){
MyThreadDemo m = new MyThreadDemo();
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
Thread t3 = new Thread(m);
t1.start();
t2.start();
t3.start();
}
}
问题描述:
上面的多线程售票出现了一下错误现象
重票:
错票:
首先,我们分析一下为什么出现这种问题呢?
我们所期望的理想状态为:
但是,会出现一个极端状态:
这是由于一个线程在操作共享数据过程中,未执行完毕的情况下,例外的线程参与进来,导致了共享数据存在了安全问题,本实例的共享数据为ticket。
那么,如何处理程序的线程安全问题呢?
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。
原理我们也理解了,那java如何去实现线程的安全呢?
使用线程的同步机制
实现方式有两种:
- 同步代码块
synchronized(同步监视器){//需要被同步的代码块(即操作共享数据的代码)}
1. 共享数据:多个线程共同操作的同一个数据(变量)
2. 同步监视器:由任何一个类的对象来充当,哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁。
代码实现:
class MyThreadDemo implements Runnable {
int ticket = 100;
Object object = new Object();
@Override
public void run() {
//Object object = new Object();//不行,由成员变量变为局部变量,每创建一个线程,都会创建一个对象
while (true){
synchronized (object){//同步监视器(锁)可以由任何一个类的对象来充当,也可以使用this,表示当前对象,只new了一次。但是在继承的方式中,一定要注意使用this,可能创建了多个线程对象,要求是多个线程使用同一个锁,即使用同一个对象。
if (ticket>0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--);
}else{
break;
}
}
}
}
}
- 同步方法
同步的方法实现在java提升2相关章节已经给出,可以前去查看。
它是将操作共享数据的方法声明为synchronized
,即此方法为同步方法,能够保证当其中一个线程执行此方法时,其它线程在外等待直至此线程执行完此方法。
那么同步方法有没有锁呢,答案是有的,锁为this。但是同样要注意,在使用继承创建的线程的方式中,同样要慎用同步方法的方式,因为它的锁为this。必须保证多个线程共用一把锁。
- 使用锁的方式(了解)
二. 互斥锁之单例模式之懒汉式的线程安全问题
互斥锁
指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 关键字synchronized
来与对象的互斥锁联系。当某个对象用synchronized
修饰时,表明该对象在任一时刻只能由一个线程访问。
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁为this。
- 同步方法(静态的)的锁为当前类本身。
单例模式懒汉式实现:
class Singleton {
private Singleton() {
}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class TestSingleton {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
output:
true
会出现线程安全问题,多个线程同时执行时,第一个线程调用getInstance()
进入if语句内,还未实例化,出现线程挂起,后面的线程也会进入会再创建一个对象,最终导致可能返回了不同Singleton对象。
解决线程安全问题实现:
public static Singleton getInstance() {
//如果第一个线程获取了单例的实例对象,后面的线程再获取实例的时候就不需要进入同步代码块了
if (instance == null) {//多线程执行时,会先判断对象是否为null,如果为null,直接返回,无需等待进去同步代码块线程执行完毕,然后再去执行,提高了效率
synchronized (Singleton.class) {//对于静态方法,使用但钱勒本身充当锁
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
综上,用这种方式解决了懒汉式的线程安全问题,也提高了效率,但是在实际开发中还是用饿汉式的比较多,因为这种方式,相对复杂,不适合应用。
三. 线程的死锁问题
死锁
,是指不同的线程分别占用对方需要的同步资源不放弃,都再等待对方放弃自己需要的同步资源,就形成了线程的死锁。
死锁的实例:
public class TestDeadLock {
static StringBuffer sb1 = new StringBuffer();
static StringBuffer sb2 = new StringBuffer();
public static void main(String[] args){
new Thread(){
public void run(){
synchronized (sb1){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb1.append("A");
synchronized (sb2){
sb2.append("B");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}.start();
new Thread(){
public void run(){
synchronized (sb2){
sb1.append("C");
synchronized (sb1){
sb2.append("D");
System.out.println(sb1);
System.out.println(sb2);
}
}
}
}.start();
}
}
四. 线程通信
- 三个方法
- wait(): 令当前线程挂起并放弃CPU,同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
- notify(): 唤醒正在排队等待同步资源的线程中优先级最高者结束等待
- notifyAll(): 唤醒正在排队等待资源的所有线程结束等待。
注:Java.lang.Object提供的这三个方法只有再synchronized方法或synchronized代码块中才能使用,否则会报java.lang.lllegalMonitorStateException异常。
实例:
/**
* 线程通信
* 使用两个线程打印1-100,线程1,线程2,交替打印
*/
class PrintNum implements Runnable {
int num = 1;
@Override
public void run() {
while (true) {
synchronized (this){
notify();
if (num <= 100) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
num++;
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class TestCommunication {
public static void main(String[] args) {
PrintNum p = new PrintNum();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.setName("甲");
t2.setName("乙");
t1.start();
t2.start();
}
}
output:
甲:1
乙:2
甲:3
乙:4
甲:5
乙:6
...
甲:97
乙:98
甲:99
乙:100
关于多线程的相关知识,暂时先到这,后续有学习的内容,会持续更新,喜欢的,加个关注白呲牙。