【Java】多线程复习

目录


0.Create a Thread

方法①

方法②(常用)

两种创建方式区别

多线程内存示意图

start和run方法的区别

1.The Status of Thread

2.Security Problems in Multithreading(多线程)

3.Four kinds of usage of Synchroized(四种修饰)

需要同步的地方

代码块

方法

静态方法

一个类

4.Multithreading Security Problems in (多线程安全问题)

                   Lazy/Hungry Man singleton pattern(单例饿汉/懒汉)

饿汉(没有新对象,用getInstance来获得)

懒汉(懒得创建新对象,用之前有的)

解决方法:

5.Consumer and Producer Problem(生产者-消费者问题)

背景前瞻

产生问题

等待唤醒机制(wait() / notify() / notifyAll())

多消费者、多生产者问题

6.Details in Multithreading

sleep()和wait()的异同点

线程如何停止

守护(后台)线程

优先级&线程组

join() & yield()

微妙的地方

 

 

0.Create a Thread


方法①

继承Thread类,重写run()方法,调用start方法开启线程(也就是调用重写的run方法)

 

 

方法②(常用)

实现Runnable接口,重写run方法,将实现类作为参数传递给Thread对象,最后调用start方法开启线程(也就是调用run方法)

Demo d = new Demo();    //Demo类实现了Runnable接口,但他还不是线程对象
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();

Thread源码

class Thread{
    private Runnable target;

    Thread(Runnable target){
        this.target = target;
    }

    public void run(){
        if(target != null){
            target.run();
        }
    }
}

**所有的线程共享实现类的成员变量

比如Demo中有一个成员变量ticket=100,t1/t2的run方法都可以对其进行操作,而且数值会保存

 

两种创建方式区别

方法①:线程任务和线程对象耦合在一起,一旦创建了Thread对象,就代表创建了线程任务以及线程对象,也就是有线程任务就有对象了

方法②:线程分为两部分:线程对象(Thread对象)+线程任务(实现Runnable接口的类),将线程任务和线程对象解耦,此方式更加面向对象,也就更常用

 

多线程内存示意图

public static void main(String[] args){
    Demo d = new Demo();

    Thread t1 = new Thread(d);
    Thread t2 = new Thread(d);

    t1.run();    //由当前线程负责
    t2.start();    //由新创建的Thread-1负责
}

【Java】多线程复习_第1张图片

 

start和run方法的区别

调用run方法不开启线程,仅是对象调用方法,由当前线程负责方法的执行;

调用start方法开启线程,并让jvm调用run方法在新开启的线程中负责方法执行。

 

 

1.The Status of Thread


*参考地址:https://www.cnblogs.com/happy-coder/p/6587092.html

 

1. 新建状态(New)         : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked)  : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead)    : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

 

2.Security Problems in Multithreading(多线程)


*根本原因也就是对共享数据操作

而要解决此多线程安全问题就用到了同步锁

public void run(){
    Object obj = new Object();

    synchroized(obj){
        //需要同步的代码块,对共享数据的操作
    }

}

在线程执行run方法之前,会要求得到对象obj,而obj只有一个。

只要其中一个线程A得到了此对象obj,即使CPU切换线程B来执行,线程B得不到对象obj也就不能执行下去。

而线程A在执行完代码块之后,就会将对象obj返回,其他线程就可以得到对象obj了。

 

使用同步锁弊端:降低了程序的性能(切换进程时得不到对象obj-->不做事)

 

可能出现的问题:同步中用的不是同一把锁(对象obj不是同一个),导致同步失效

 

 

3.Four kinds of usage of Synchroized(四种修饰)


需要同步的地方

同步里面一般都是循环,如果不是循环,瞬间执行完了就没必要多线程了

一般都是同步操作共享数据的代码,所以用同步来修饰的地方既不能过大(可能变成单线程),也不能过小(线程安全问题)

 

代码块

被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象(this),可以在括号内指定锁Synchroized(Obj obj){},锁是obj

 

方法

被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象(this)

 

静态方法

作用的范围是整个静态方法,作用的对象是这个类的所有对象(类.class这个对象)

 

一个类

其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象(类.class这个对象)

 

 

4.Multithreading Security Problems in (多线程安全问题)

                   Lazy/Hungry Man singleton pattern(单例饿汉/懒汉)


饿汉(没有新对象,用getInstance来获得)

class Single{
    
    private static Single s = new Single();
    
    private Single();

    public static Single getInstance(){
        
            return s;
    }

}

 

懒汉(懒得创建新对象,用之前有的)

**多线程并发问题:线程0判断完s=null,CPU切换执行权到线程1判断s=null后创建对象s,切换到线程0之后又创建对象s

class Single{
    
    private static Single s = null;
    
    private Single();

    //加入synchronized解决多线程同步问题,但是影响了效率
    public static /*synchronized*/ Single getInstance(){
        if(s==null){
            s = new Single();
            return s;
        }
    }

}

 

解决方法:

加入synchronized(解决并发问题)

如上述代码注释部分

 

双重判断,减少判断锁的次数(解决加同步后效率低下问题)

减少判断锁的次数:顾名思义,也就是减少执行synchronized()这一行或synchronized修饰的方法

双重判断最核心的地方:第一个线程创建完对象之后,其他线程不用再判断锁(因为在第一个判断中就已经出去了)

class Single{
    
    private static Single s = null;
    
    private Single();

   //双重判断
    public static  Single getInstance(){
        if(s==null){
            synchronized(Single.class){
                if(s==null){
                    s = new Single();
                    return s;
                }
            }
        }else return s;
    }

}

 

 

5.Consumer and Producer Problem(生产者-消费者问题)


背景前瞻

生产者线程每生产一个资源,由消费者线程消费,然后生产者线程再生产……(一个生产者、一个消费者)

class Resource{
	private String name;
	
	public synchronized void set(String name) {
		
		this.name = name;
		System.out.println(Thread.currentThread().getName() + "生产了" + this.name);

	}
	
	public synchronized void out() {
		System.out.println(Thread.currentThread().getName() + "消费了" + this.name);

	}
}

class Customer implements Runnable{

	private Resource r ;
	
	Customer(Resource r){
		this.r = r;
	}
	
	
	public void run() {
		while(true) {
			r.set("面包");
		}
	}
	
}

class Producer implements Runnable{

	private Resource r ;
	
	Producer(Resource r){
		this.r = r;
	}
	
	
	public void run() {
		while(true) {
			r.out();
		}
		
	}
	
}

public class PAndC {
	public static void main(String[] args) {
		Resource r = new Resource();
		Producer p1 = new Producer(r);
		Customer c1 = new Customer(r);
		
		Thread t1 = new Thread(p1);
		Thread t2 = new Thread(c1);
		
		t1.start();
		t2.start();
		
	}
}

 

产生问题

生产者生产多次,才到消费者消费多次

*解决:等待唤醒机制

通过flag标记,如果flag为真(消费者wait(),生产者执行并notify()),如果flag为假(生产者wait(),消费者执行并notify())

class Resource{
	private String name;
	private boolean flag = false;
	
	public synchronized void set(String name) {
		if(flag)try {wait();}catch(Exception e) {};
		this.name = name;
		System.out.println(Thread.currentThread().getName() + "生产了" + this.name);
		flag = true;
		notify();
	}
	
	public synchronized void out() {
		if(!flag)try {wait();}catch(Exception e) {};
		System.out.println(Thread.currentThread().getName() + "消费了" + this.name);
		flag = false;
		notify();
	}
}

 

 

等待唤醒机制(wait() / notify() / notifyAll())

**这三个方法必须使用在同步(synchronized)中,因为要标识这些方法所属的锁,通过锁来判断wait()或者notify()哪个线程池

wait():将调用此方法的线程临时存储到线程池中

notify():会唤醒线程池中任一 一个线程

notifyAll():会唤醒线程池中所有线程

 

*同一个锁上的notify(),只能唤醒该锁上的被wait()的线程

 

上述解决方法中,对资源对象(单例)锁上,调用set的进程0,调用out的进程1分别临时存储到线程池中,因为只有一个资源对象,所以可以判断是同一个线程池。

 

多消费者、多生产者问题

*问题:消费者唤醒的对象可能是消费者,也可能是生产者(我们需要的是唤醒不同方)

唤醒同方会导致生产两次 or 消费两次

*解决:while替代if的判断(多消费者、生产者必备while)

 

*新问题:替代之后导致死循环。eg:生产者唤醒生产者之后,占着资源,死循环(消费者还未将标识变换,本应是唤醒消费者的)

 

*解决:唤醒所有(notifyAll()),但是效率低,因为又唤醒了本方

 

*效率低解决办法:jdk1.5之后提供的java.util.concurrent.locks代替同步

Lock接口:lock()获得锁、unlock()释放锁 用于代替synchronized

Condition接口:await(),singal(),singalAll() 用于代替wait()、notify()、notifyAll()

*同操作系统中的P/V操作是一个概念,Condition对象就是信号量

class Resource{
	private String name;
	private boolean flag = false;    //判断是否需要停止生产者或者消费者的标识
	private Lock lock = new ReentrantLock();
	private Condition p = lock.newCondition();	//生产
	private Condition c = lock.newCondition();	//消费
	
	public  void set(String name) {
		lock.lock();
		try {
			while(flag)try {p.await();}catch(Exception e) {};
			this.name = name;
			System.out.println(Thread.currentThread().getName() + "生产了" + this.name);
			flag = true;
			c.signal();
		}finally {
			lock.unlock();
		}
		
	}
	
	public  void out() {
		lock.lock();
		try {
			while(!flag)try {c.await();}catch(Exception e) {};
			System.out.println(Thread.currentThread().getName() + "消费了" + this.name);
			flag = false;
			p.signal();
		}finally {
			lock.unlock();
		}
		
	}
}

 

 

6.Details in Multithreading


 

sleep()和wait()的异同点

  • sleep()必须指定时间;wait()可以指定也可以不指定时间
  • sleep()时间到,线程处于临时阻塞或运行状态;wait()如果没有时间,需要通过notify()或notifyAll()唤醒
  • sleep()不一定要定义在同步中;wait()必须定义在同步中
  • 定义在同步中时,线程执行sleep(),不会释放锁(其他线程执行不了);线程执行到wait(),会释放锁

 

 

线程如何停止

stop():过时,因为执行之后会释放其拥有的所有锁,会引发其他问题,所以被弃用

*interrupt():将线程的冻结状态清除,让线程恢复到正常运行状态,因为是强制性的所以会自带异常

**将notify()比喻作唤醒一个睡觉的人;interrupt()就是一棍子打醒睡觉的人,打出来的包就是异常。

线程结束:让run()结束(线程处于冻结[wait()]状态时无法判断标识flag,没人唤醒线程)

  • 方法①:run()中通常都是循环,在线程外面控制一个标识来控制循环就可以让线程结束
  • 方法②:interrupt()打断之后在catch(){...}中控制标识来控制循环来让线程结束

 

守护(后台)线程

和一般线程(前台)一样,在启动线程之前需要(thread.setDaemon(true))就可以变成后台线程

*特点:线程结束方式:

  • 执行完run()方法
  • 所有前台线程运行完-->进程运行完

 

 

优先级&线程组

默认优先级5,数字表示1-10,明显的优先级1,5,10

serPriority(Thread.MAX_PRIORITY);

 

线程组:可以对多个同组的线程,进行统一的操作(比如interrupt())

 

 

join() & yield()

t1.join():主线程释放执行权,让其余的线程抢夺执行权,只有在t1执行完之后主线程才能获得执行权(处于冻结状态)

yield():释放CPU执行权,让其他线程有机会获取(自己也能再次获取)。让线程放缓,增加间隔性

 

 

微妙的地方

new Thread(new Runnable(){
      public void run(){
            System.out.println("runnable run");
      }
}){
      public void run(){
            System.out.println("subthread run");
      }
}.start();

new Thread() {.....}创建了Thread的子类重写了父类的run方法

父类的run方法原本是:

class Thread{
    private Runnable r;
    
    Thread(Runnable r){
        this.r = r;
    }
    
    public void run(){
        if(r != null){
            r.run();
        }
    }

    public void start(){
        run();
    }

}

所以重写了之后,最后输出的结果的subthread run

你可能感兴趣的:(JAVA)