多线程学习-02

线程安全问题与线程同步

1、什么是线程安全问题

之前学习常用类时:
 StringBuffer与StringBuilder
 ArrayList与Vector
 HashMap与Hashtable
和上一篇的卖票问题,出现负数票
导致线程安全问题:多个线程访问同一份资源
问题出现总结:
1、多个线程
2、共享数据
3、多条语句访问或操作共享数据

2、如何解决线程安全问题

解决思路:
   对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
解决办法:
如果想要解决线程安全问题,就必须使用同步。所谓的同步就是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待
此线程完成之后才可以继续执行。

3、同步的几种方式

(1)同步代码块
    格式:多个线程需要使用唯一的一把锁
    synchronized(锁对象){
     需要同步的代码
    }
(2)同步方法
   格式:synchronized 方法其他修饰符 返回值类型 方法名(形参列表)throws 异常列表{}

1.同步代码块

package com.synch;

public class TestTicket {
	public static void main(String[] args) {
		Ticket t = new Ticket();
		//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
		new Thread(t,"窗口一").start();
		new Thread(t,"窗口二").start();
		new Thread(t,"窗口三").start();
	}
}
class Ticket implements Runnable{
	private int num = 10;

	public void run(){
		while(true){
			synchronized (this) {
				if(num<=0){
					break;
				}
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
			}
		}
	}
}

2.同步方法

package com.synch;

public class TestTicket {
	public static void main(String[] args) {
		Ticket t = new Ticket();
		//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
		new Thread(t,"窗口一").start();
		new Thread(t,"窗口二").start();
		new Thread(t,"窗口三").start();
	}
}
class Ticket implements Runnable{
	private int num = 10;
	private boolean flag =true;

	public void run(){
		while(flag){
			sale();
		}
	}
	synchronized public void sale(){
		if(num<=0){
			flag = false;
			return;
		}
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
	}
}

4、同步需要注意的问题

(1)同步锁

同步锁机制:

在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 
防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁
之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

synchronized的锁是什么?

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class)、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:

必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

(2)同步的范围

如何找问题,即代码是否存在线程安全?(非常重要)

(1)明确哪些代码是多线程运行的代码

(2)明确多个线程是否有共享数据

(3)明确多线程运行代码中是否有多条语句操作共享数据的

2、如何解决呢?(非常重要)

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

即所有操作共享数据的这些语句都要放在同步范围中

切记:

  范围太小:没锁住所有有安全问题的代码
  范围太大:没发挥多线程的功能
public class TestSynchProblem {
	public static void main(String[] args) {
		Window t = new Window();
		//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
		new Thread(t,"窗口一").start();
		new Thread(t,"窗口二").start();
		new Thread(t,"窗口三").start();
	}
}
class Window implements Runnable{
	private int num = 10;

	//范围太大,导致只有一个窗口可以卖票
	public void run(){
		while(true){
			//多个线程不是同一把锁
			synchronized(new Object()){//synchronized((Integer)num){[		int num = 10;
		Integer i = (Integer)num;
		num = 9;
		Integer j = (Integer)num;
		System.out.println(i==j);//false]
				if(num<=0){
					break;
				}
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
			}
		}
	}
}
class Window implements Runnable{
	private int num = 10;

	public void run(){
		while(num>0){//num>0的条件也属于访问共享数据的代码,没有放入同步范围,范围太小,数据仍然不安全
			synchronized (this) {
				//if(num>0){//解决方案,可以在里面再判断一次条件
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
//				}else{
//					break;
//				}
			}
		}
	}
}
class Window implements Runnable{
	private int num = 10;

	//范围太大,导致只有一个窗口可以卖票
	synchronized public void run(){
		while(num>0){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
		}
	}
}

(3)同步的优点与缺点

同步的好处:解决了多线程的安全问题

副作用:多个线程需要等待锁,因此性能低

5、释放锁和不释放锁

释放锁的操作

  当前线程的同步方法、同步代码块执行结束。
  当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
  当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

不会释放锁的操作

  1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
  2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会
释放锁(同步监视器)。
     1.应尽量避免使用suspend()和resume()这样的过时来控制线程

6、死锁(遏制彼此命门,死活不放

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

解决方法:

 专门的算法、原则
 尽量减少同步资源的定义
 尽量避免嵌套同步

7、显示锁Lock

   java从1.5版本之后,提供了Lock接口。在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。
所以将这些动作定义在了锁当中,并把锁定义成对象。所以,同步是隐式的锁操作,而Lock对象是显示的锁操作。
synchronized是JVM层面提供的锁,而Lock是Java的语言层面jdk为我们提供的锁,这些锁都在 Java.util.concurrent包中。
先来看一下JVM提供的锁和并发包中的锁有哪些区别:
1.synchronized的加锁和释放都是由JVM提供,不需要我们关注,而lock的加锁和释放全部由我们去控制,
通常释放锁的动作要在finally中实现。
2.synchronized只有一个状态条件,也就是每个对象只有一个监视器,如果需要多个Condition的组合那么synchronized
是无法满足的,而Lock则提供了多条件的互斥,非常灵活。
3.ReentrantLock(可重入锁,锁的一种) 拥有Synchronized相同的并发性和内存语义,
此外还多了锁投票,定时锁等候和中断锁等候。(锁的种类有很多)
package com.lock;

import java.util.concurrent.locks.ReentrantLock;

public class TestLock {

	public static void main(String[] args) {
		Ticket t = new Ticket();
		//三个线程共享了t对象,num就只有一份,每个线程都可能修改它,一旦其中一个线程修改它,那么就会影响其他线程
		new Thread(t,"窗口一").start();
		new Thread(t,"窗口二").start();
		new Thread(t,"窗口三").start();
	}

}
class Ticket implements Runnable{
	private int num = 10;
	private final ReentrantLock lock = new ReentrantLock();
	
	public void run(){
		while(true){
			try {
				lock.lock();//获取锁
				if(num<=0){
					break;
				}
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"抢到了一张票,剩余:"+(--num));
			} catch (Exception e) {
				e.printStackTrace();
			}finally{
				lock.unlock();//释放锁
			}
		}
	}
}

idea枷锁方式快捷键:ctrl+alt+t

猜你喜欢

转载自blog.csdn.net/weixin_43549578/article/details/83928411