48--多线程(七)

线程安全问题

多个线程执行的不确定性引起执行结果的不稳定 ,在项目运行过程中会有多个线程同时操作同一资源的情况,此时就会引发同步问题。这里以前面说的卖票为例
假设这里有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有了新的解决方案(后续)。
未完待续
=============================================================================================
如有不妥之处,欢迎大家给予批评指出,如果对您有帮助,给留下个小赞赞哦
==============================================================================================

猜你喜欢

转载自blog.csdn.net/qwy715229258163/article/details/114907350