线程安全问题
多个线程执行的不确定性引起执行结果的不稳定 ,在项目运行过程中会有多个线程同时操作同一资源的情况,此时就会引发同步问题。这里以前面说的卖票为例:
假设这里有10张票要出售,分别交给三个窗口分别销售:
实例:继承Thread类方式
package com.qwy;
class Window extends Thread{
private static int ticket= 10;
@Override
public void run() {
while(true){
//假设剩余票的数量为票号
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出票号:"+ticket--);
}else{
break;
}
}
}
}
public class TestTick{
public static void main(String[] args) {
//创建三个线程对象,表示三个窗口
Window w1= new Window();
Window w2= new Window();
Window w3= new Window();
//给三个窗口起名(设置线程名称)
w1.setName("窗口A");
w2.setName("窗口B");
w3.setName("窗口C");
//启动线程
w1.start();
w2.start();
w3.start();
}
}
可能运行的结果:
窗口A卖出票号:10
窗口B卖出票号:9
窗口B卖出票号:8
窗口B卖出票号:7
窗口B卖出票号:6
窗口B卖出票号:5
窗口A卖出票号:4
窗口B卖出票号:3
窗口B卖出票号:1
窗口A卖出票号:2
窗口C卖出票号:1
问题:从运行结果发现票号1被重复卖 了。
实例:实现Runnable接口方式
package com.qwy8;
class Window implements Runnable{
private int ticket= 10;
@Override
public void run() {
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出票号:"+ticket--);
}else{
break;
}
}
}
}
public class TestTickit {
public static void main(String[] args) {
//创建Runnable接口实例
Window w1 = new Window();
//创建三个线程表示三个窗口
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
//给线程起名
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
可能的运行结果:
窗口C卖出票号:10
窗口A卖出票号:8
窗口B卖出票号:9
窗口A卖出票号:7
窗口C卖出票号:6
窗口B卖出票号:5
窗口A卖出票号:4
窗口C卖出票号:3
窗口B卖出票号:2
窗口C卖出票号:-1
窗口B卖出票号:0
窗口A卖出票号:1
问题 :本例中加入了休眠,从打印结果看卖出了不合法的票号。
以上两个例子中无论是继承Thread类还是实现Runnable接口的方式都存在操作统一资源时,存在线程安全问题。即:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以 参与执行。
线程的同步(synchronized)
Java对多线程的安全问题提供了解决方案:同步机制
1.同步代码块
synchronized(同步监视器对象){
//需要同步的代码;
}
2.public synchronized void method (String name){ …. }
实例:使用同步代码块,解决继承Thread方式的安全问题
package com.qwy8;
class Window extends Thread{
private static int ticket= 10;
@Override
public void run() {
while(true){
/**
* synchronized(Window.class)
* Window.class:同步监视器,可以是任何对象,但是这里因为是继承Thread的类的方式
* 实现的同步代码块,所以这个例子中不能使用this作为同步监视器(锁)
* this在这个例子中代表w1,w2,w3,所以同步监视器(锁)不是同一个。
*
*/
synchronized(Window.class){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出票号:"+ticket--);
}else{
break;
}
}
}
}
}
public class TestTick{
public static void main(String[] args) {
//创建三个线程对象,表示三个窗口
Window w1= new Window();
Window w2= new Window();
Window w3= new Window();
//给三个窗口起名(设置线程名称)
w1.setName("窗口A");
w2.setName("窗口B");
w3.setName("窗口C");
//启动线程
w1.start();
w2.start();
w3.start();
}
}
运行可能结果:
窗口A卖出票号:10
窗口A卖出票号:9
窗口C卖出票号:8
窗口C卖出票号:7
窗口B卖出票号:6
窗口B卖出票号:5
窗口B卖出票号:4
窗口B卖出票号:3
窗口C卖出票号:2
窗口C卖出票号:1
实例:上例代码的同步监视器(锁)可以换成以下代码
package com.qwy8;
class Window extends Thread{
private static int ticket= 10;
private static Object obj = new Object();
@Override
public void run() {
while(true){
/**
* 这里使用obj作为同步监视器(锁),且obj必须使用static修饰,
* 原因:必须保证多个线程使用同一个监视器
*/
synchronized(obj){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出票号:"+ticket--);
}else{
break;
}
}
}
}
}
public class TestTick{
public static void main(String[] args) {
//创建三个线程对象,表示三个窗口
Window w1= new Window();
Window w2= new Window();
Window w3= new Window();
//给三个窗口起名(设置线程名称)
w1.setName("窗口A");
w2.setName("窗口B");
w3.setName("窗口C");
//启动线程
w1.start();
w2.start();
w3.start();
}
}
实例:使用同步代码块,解决实现Runnable接口方式的安全问题
package com.qwy9;
class Window implements Runnable{
private int ticket= 10;
private Object obj= new Object();
@Override
public void run() {
while(true){
/**
* synchronized(obj)此时同步监视器(锁)可以使用成员表变量obj,且可以不同static修饰
* synchronized(this);可以使用this,此时this表示Window的对象
* 两种方式都可以用的原因:三个线程t1,t2,t3都使用了唯一的对象w1
* synchronized(Window.class):也可以
*
*/
synchronized(this){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出票号:"+ticket--);
}else{
break;
}
}
}
}
}
public class TestTickit {
public static void main(String[] args) {
//创建Runnable接口实例
Window w1 = new Window();
//创建三个线程表示三个窗口
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
//给线程起名
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
可能的运行结果:
窗口A卖出票号:10
窗口A卖出票号:9
窗口A卖出票号:8
窗口A卖出票号:7
窗口B卖出票号:6
窗口B卖出票号:5
窗口C卖出票号:4
窗口C卖出票号:3
窗口B卖出票号:2
窗口A卖出票号:1
实例:使用同步代方法,解决继承Thread类方式的安全问题
package com.qwy10;
class Window extends Thread {
private static int ticket = 10;
private static Object obj = new Object();
@Override
public void run() {
//为了能将票卖完,这里多循环了
for(int i=0;i<1000;i++){
sell();
}
}
/**
* 同步方法,这里因为使用了继承Thread的方式必须使用static修饰方法:
* private static synchronized void sell(),此时同步监视器为Window.class
* 不使用static修饰是错误的:
* private synchronized void sell()此时的同步监视器为w1,w2,w3不是同一个
*
*/
private static synchronized void sell() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出票号:" + ticket--);
}
}
}
public class TestTick {
public static void main(String[] args) {
// 创建三个线程对象,表示三个窗口
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
// 给三个窗口起名(设置线程名称)
w1.setName("窗口A");
w2.setName("窗口B");
w3.setName("窗口C");
// 启动线程
w1.start();
w2.start();
w3.start();
}
}
可能运行结果:
窗口A卖出票号:10
窗口C卖出票号:9
窗口B卖出票号:8
窗口C卖出票号:7
窗口A卖出票号:6
窗口C卖出票号:5
窗口B卖出票号:4
窗口C卖出票号:3
窗口A卖出票号:2
窗口C卖出票号:1
实例:使用同步代方法,解决实现Runnable接口方式的安全问题
package com.qwy11;
class Window implements Runnable {
private int ticket = 10;
@Override
public void run() {
for(int i=0;i<1000;i++){
sell();
}
}
/**
* 同步方法,此时不需要使用static修饰,同步监视器为this
*/
private synchronized void sell() {
//
// synchronized(this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出票号:" + ticket--);
}
// }
}
}
public class TestTickit {
public static void main(String[] args) {
// 创建Runnable接口实例
Window w1 = new Window();
// 创建三个线程表示三个窗口
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
// 给线程起名
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
可能的运行结果:
窗口A卖出票号:10
窗口A卖出票号:9
窗口A卖出票号:8
窗口C卖出票号:7
窗口C卖出票号:6
窗口C卖出票号:5
窗口B卖出票号:4
窗口B卖出票号:3
窗口C卖出票号:2
窗口C卖出票号:1
同步机制中的锁总结:
同步锁机制:
对于并发工作,需要某种方式来防止两个任务访问相同的资源(其实就是共享资源的竞争)。为了防止这种冲突的方法就是当资源被一个任务使用时,在其上加上锁。第一个访问某项资源的任务必须锁定这个资源,使其他任务在其被解锁之前,就无法访问他了。而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁解释:
1.任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)
2.同步方法的锁:静态方法(类名.class)、非静态方法(this)
3.同步代码块:自己制定,很多时候也可以指定为this或类名.class
注意:
1.必须确保使用同一资源的多个线程公用一把锁,这个非常重要,否则就无法保证资源的安全
2.一个线程类中所有的静态方法共同使用同一把锁(类名.class),所有非静态方法共同使用同一把锁(this),同步代码块(指定需要谨慎)
释放锁的操作
1.当前线程的同步方法、同步代码块执行结束
2. 当前线程在同步代码块、同步方法中遇到了break,return终止了该代码块、该方法的继续执行
3.当前线程在同步代码、同步方法中出现了未处理的Error或Exception,导致异常结束
4.当前线程在同步代码块、同步方法中执行了线程对象的wait()方法。当前线程暂停并释放锁。
不会释放锁的操作
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yieId()方法暂停当前线程的执行。
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程 挂起,该线程不会释放锁(同步监视器)。
3.应尽量避免使用suspend()和resume()来控制线程。
线程的死锁问题
死锁:
1.不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁
2.出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
解决方法:
1.专门的算法、原则
2.尽量减少同步资源的定义
3.尽量避免嵌套同步
死锁实例:
package com.qwy12;
class A {
public synchronized void a(B b){
System.out.println(Thread.currentThread().getName()+"企图调用B中的print方法");
b.printB();
}
public synchronized void printA(){
for(int i=0;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
class B{
public synchronized void b(A a){
System.out.println(Thread.currentThread().getName()+"企图调用A中的print方法");
a.printA();
}
public synchronized void printB(){
for(int i=0;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
class MyThread implements Runnable{
private A a= new A();
private B b= new B();
@Override
public void run() {
a.a(b);
b.b(a);
}
}
public class DeadLockTest {
public static void main(String[] args) {
MyThread my= new MyThread();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.setName("A线程");
t2.setName("B线程");
t1.start();
t2.start();
}
}
可能死锁的一种输出结果:
B线程企图调用B中的print方法
B线程–0
B线程–1
B线程–2
B线程–3
B线程–4
B线程–5
B线程–6
B线程–7
B线程–8
B线程–9
B线程–10
B线程企图调用A中的print方法
A线程企图调用B中的print方法
此时程序卡主,不再执行。也不会中断执行。
以上的线程安全操作都是在JDK1.5之前的解决方案。JDK1.5有了新的解决方案(后续)。
未完待续
=============================================================================================
如有不妥之处,欢迎大家给予批评指出,如果对您有帮助,给留下个小赞赞哦
==============================================================================================