Java 线程安全问题
当我们使用多个线程访问同一资源 (可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作
,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写
的操作,就容易出现线程安全问题。
实际举例
开启三个窗口售票,总票数为100张。(相当于多线程)
java//实现方式一: 实现Runnable接口 public class WindowTest { public static void main(String[] args) { SaleTicket s = new SaleTicket(); Thread t1 = new Thread(s); Thread t2 = new Thread(s); Thread t3 = new Thread(s); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class SaleTicket implements Runnable{ int ticket = 100; @Override public void run(){ while (true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket); ticket --; }else{ break; } } } } //输出结果 //存在不同窗口卖出相同票号的问题 //出现了线程的安全问题
java//实现方式二: 继承Thread类 public class WindowTest1 { //使用继承Thread类实现 public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口1"); w2.setName("窗口2"); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } } class Window extends Thread{ static int ticket = 100; @Override public void run() { while(true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket); ticket--; }else{ break; } } } } //即使使用static 修饰ticket 仍然存在同票现象
归纳问题
-
多线程卖票出现的问题
出现了同票和错票现象
-
什么原因导致?
线程1操作ticket的过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作
-
如何解决?
必须保证一个线程A在操作ticket的过程中,其他线程必须等待,直到线程A操作ticket结束以后,其他线程才能进来继续操作ticket。
-
Java是如何解决线程安全问题的?
使用线程的同步机制。
同步代码块
和同步方法
同步代码块
同步代码块概述
synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
java
synchronized(同步监视器){ //多个线程必须共用同一个同步监视器,当线程类只有一个实例化时候,可以使用this
//需要被同步的代码
}
说明
- 需要被同步的代码,即为操作共享数据的代码
共享数据,即多个线程都需要操作的数据
- 需要被同步的代码,在被
synchronized
包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。- 同步监视器,俗称锁。哪个线程获得了锁,哪个线程就能执行需要被同步的代码。
- 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。
使用同步代码块修改程序
使用同步代码块修改上述实现
Runnalbe
接口的线程问题
java
//使用同步代码块解决上述线程问题
class SaleTicket implements Runnable{
int ticket = 100;
@Override
public void run() {
while(true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){ //SaleTicket只有唯一类,可以使用this
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
ticket--;
}
else {
break;
}
}
}
}
}
public class Test{
public static void main(String []args){
SaleTicket p = new SaleTicket();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
Thread t3 = new Thread(p);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
使用同步代码块解决继承
Thread
类的线程安全问题
java
//使用继承Thread类的方式,实现卖票
//使用同步代码块解决卖票的线程安全问题
public class Test2 {
public static void main(String[] args) {
SaleTicket2 t1 = new SaleTicket2();
SaleTicket2 t2 = new SaleTicket2();
SaleTicket2 t3 = new SaleTicket2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class SaleTicket2 extends Thread{
static int ticket = 100; //ticket必须为静态变量
public void run() {
while(true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (SaleTicket2.class){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
ticket--;
}
else {
break;
}
}
}
}
}
同步方法
同步方法概述
同步方法说明
如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
**同步方法:**synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。
java
public synchronized void method(){
//可能会产生线程安全问题的代码
}
//声明了方法后,要在run()方法中调用method()
使用同步方法修改程序
实现Runnable接口
java
//使用实现Runnable接口的方式 实现多窗口卖票问题
//使用同步方法解决多窗口中的线程安全问题
public class WindowTest1 {
public static void main(String[] args) {
Window p = new Window();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
Thread t3 = new Thread(p);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t2.start();
t1.start();
t3.start();
}
}
class Window implements Runnable{
int ticket = 100;
boolean isFlag = true;
public synchronized void show(){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
ticket--;
}
else {
isFlag = false;
}
}
@Override
public void run() {
while(isFlag){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
show();
}
}
}
继承Thread类方式
java
//使用继承Thread类的方式,实现多窗口售票的模式
//使用同步方法,解决线程安全问题
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window2 extends Thread{
static boolean isFlag = true;
static int ticket = 100;
//使用继承Thread类的方式,则多线程会声明多个实例化对象,ticket必须使用static修饰
@Override
public void run() {
while(isFlag){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
show();
}
}
public static synchronized void show(){ //必须使用static的同步监视器
//此时的同步监视器是 当前类本身(加了static)
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"售票,票号为"+ticket);
ticket--;
}else{
isFlag = false;
}
}
}
说明
- 如果操作共享数据的代码完整的声明在了一个方法中,那么就可以将此方法声明为同步方法即可
- 非静态的同步方法,默认同步监视器是
this
; 静态的同步方法,默认同步监视器是当前类本身
汇总说明
线程创建的方式有两种,分别为
实现Runnalbe接口
和继承Thread类
,对于不同的线程创建类,使用的同步方法和同步代码块也有不同
当线程创建为
继承Thread类
时,需要注意:使用实现Runnalbe接口和同步代码块时:
synchronized (this)
同步代码块中同步监视器可以使用this
替代使用实现Runnable接口和同步方法时:
将公共代码部分,封装到一个方法中,方法格式为(可修改)
public synchronized void method()
,并在pubilc void run()
中调用即可使用继承Thread类时:
公共的属性需要用static修饰 例如
static int ticket = 100
synchronized ()
同步监视器中不能使用this, 可以使用synchronized(类名.class)
替代,例如synchronized (SaleTicket2.class)
- 若继续使用同步方法时,同步方法也需要使用static进行修饰
synchronized利弊分析
- 好处,解决了线程安全问题
- 弊端,在操作共享数据时,多线程其实时串行执行的,意味着性能的降低。