JavaSE实战——多线程

转载请声明出处:http://www.voidcn.com/article/p-zaaattae-rz.html

说在前面

    在介绍java的多线程技术之前,我们先来看一下几个概念。

    硬盘:持久化数据存储设备(寻道,磁盘磁头,运算速度慢)
    内存:临时性数据存储设备(寻址,颗粒晶片,运算速度快)<-- CPU过来运算

    进程:就是应用程序在内存中分配的空间。(正在运行中的程序)
    线程:是进程中负责程序执行的执行单元。也成为执行路径,控制单元。
    进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。一个进程中至少有一个线程在负责该进程的运行。如果一个进程中出现了多个线程,就称该程序为多线程程序。
    举例:运动场--鸟巢。水立方。

    多线程技术:解决多部分代码同时执行的需求。可以合理地使用cpu资源。

    单核cpu
    相比多核,单核不具备真正的线程级并行能力。
    比如在单核上开了多个线程,这多个线程是会分时使用CPU的,在某一时刻,单核只可能为某一个线程服务,不存在并行。
    只是cpu以ms级甚至更低的时间片的方式在做非常快速的切换,让你感觉像是同时执行多个线程。且切换具有不确定性。
    此外,此时的线程也都是串行程序,不涉及并行算法,不涉及并行计算,本质仍是串行的。
    多核cpu
    多核的并行处理是真的。
    假设有4个核,不开超线程,那么同时可以执行4个线程,是真正的并行执行。
    由于并行执行,也引入了一些新的单核不存在的问题,比如负载均衡、优先级调度等等。这就是真的并行计算的范畴了。
    所以,论并行“能力”,多核是要强的,而单核的单线程能力往往要比多核里面的核要强。
    但“能力”是一回事,真正用起来后的“性能”是另一回事。未经并行设计的程序/软件跑起来很难真正用满多核的性能,在多核上跑单线程程序效果肯定不好,此外,即使一个程序线程数很多,如果这些线程无法并发,那依然不行。所以并行计算是机遇与挑战并存的。

    多线程的运行是根据cpu的切换完成的。怎么切换cpu说的算,所以多线程运行有随机性。
    cpu随机性原理:cpu的快速切换造成的,哪个线程获取到了cpu的执行权,哪个线程就执行。

jvm中的多线程

    jvm中至少有两个线程:
    一个是负责自定义代码运行的。这个从main方法开始执行的线程称之为主线程。主线程执行的代码都在main方法中。
    一个是负责垃圾回收的。当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样,会出现主线程中的代码执行会停止,会去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。 
    至于某一时刻到底哪个线程在运行,cpu说了算。

    我们来看下面这段代码:

class Demo{
	//定义垃圾回收方法
	public void finalize(){
		System.out.println("demo ok");
	}
}
class FinalizeDemo{
	public static void main(String[] args){
		new Demo();
		new Demo();
		new Demo();
		System.gc();//启动垃圾回收器。垃圾回收线程是后台线程(守护进程),随着其他线程的结束而自动结束。
		System.out.println("Hello Threads");
	}
}
    运行结果如下:


    通过实验,会发现每次结果不一定相同,那是因为cpu的随机性切换造成的。而且每一个线程都有自己运行的代码内容。这个称之为线程的任务。

    我们之所以创建一个线程就是为了去运行指定的任务代码。而线程的任务都封装在特定的区域中。
    比如:
    主线程运行的任务都定义在main方法中,这个是jvm定义的。
    垃圾回收线程在收垃圾时都会运行对象自定义的finalize方法。
    Thread类中的
run()方法,用于存储自定义线程要运行的代码。

创建线程方式一:继承Thread类

    我们先来看一个单线程的例子。下面一段代码,除了垃圾回收线程之外,只有主线程。所以d1.show执行完之后,才会执行d2.show。

class Demo{
	private String name;
	Demo(String name){
		this.name = name;
	}
	public void show(){
		for(int i = 0; i < 10; i++)
			System.out.println(name+"..."+i);
	}
}
class ThreadDemo{
	public static void main(String[] args){
		Demo d1 = new Demo("张三");
		Demo d2 = new Demo("麻子");
		d1.show();
		d2.show();
	}
}

    输出为:

    那么,如何开辟一个执行路径呢?
    通过查阅API文档 java.lang.Thread类。该类的描述中有创建线程的两种方式。
    第一种就是继承Thread类

    步骤:
    1.继承Thread类。
    2.覆盖run方法。
    3.创建子类对象就是创建线程对象。
    4.调用Thread类中的start方法就可以执行线程。并会调用run方法。

class Demo extends Thread{
	private String name;
	public Demo(String name){
		super();//父类构造函数Thread(),会生成gname线程名称""Thread-n""。可以使用父类的getName()方法获取。
		this.name = name;
	}
	//覆盖run方法
	public void run(){
		for (int x = 1; x <= 40; x++){
			System.out.println(Thread.currentThread().getName()+"...."+name+"...."+x);//如果仅仅是getName(),获得的只是当前对象的gname,不一定是当前正在运行的线程的gname。
		}
	}
	/*public void start(){//覆盖了Thread类的start方法后,就不能够启动线程了。这时只有main主线程运行。
		//super.start();//但如果有这句,就可以启动线程了。使用Thread类的start方法,即可开启。
		this.run();//这里,被覆盖的start()方法只有运行run的能力,没有启动线程的能力。这时就只是普通的对象在调用自己的成员方法而已。
	}*/
}
class ThreadDemo{
	public static void main(String[] args){//主线程栈->main方法->程序入口
		Demo d1 = new Demo("张三");//创建d1线程对象。名称gname被设置为 Thread-0,可以通过调用Thread类的getName()方法获取该名称。
		Demo d2 = new Demo("麻子");//创建d2线程对象。名称gname被设置为 Thread-1,子类自定义名称name被设置为 麻子。
		d1.start();//start():两件事:1.开启d1线程栈,2.调用run方法。(主线程开辟的d1、d2线程栈区)
		d2.start();//主线程开启d2线程栈。但是当前时刻是否执行该线程该run方法,cpu说了算。
		
		for (int x = 1; x < 40; x++){
			System.out.println(Thread.currentThread().getName()+"----"+x);
		}
	}
}

    程序输出:

    start()开启线程后,都会执行run方法,说明run方法中存储的是线程要运行的代码。所以,记住,自定义线程的任务代码都存储在run方法中。(而主线程的代码存储在main方法中)

    返回当前线程的名称:Thread.currentThread().getName()
  线程的名称是由:Thread—编号定义的。编号从0开始。

    内存图解:

    调用Thread类start和调用run方法的区别?
    调用Thread类start会开启线程,让开启的线程去执行run方法中的线程任务。
    直接调用run方法,线程并未开启,去执行run方法的只有主线程main。

多线程的五种状态

    被创建:start()
    运行:具备执行资格,同时具备执行权;
    冻结:sleep(time)-sleep(time over),wait()—notify()唤醒;线程释放了执行权,同时释放执行资格;
    临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
    消亡:stop()

多线程示例一:售票示例

    卖票的代码需要被多个线程执行,所以要将这些代码定义在线程任务中。run方法。代码如下:

<span style="font-size:14px;">class SaleTicket extends Thread{
	private int tickets = 100;
	public void run(){
		while(true){
			if (tickets > 0){
				System.out.println(Thread.currentThread().getName()+"...."+tickets--);
			}
		}
	}
}
class TicketDemo{
	public static void main(String[] args){
		SaleTicket t1 = new SaleTicket();
		SaleTicket t2 = new SaleTicket();
		SaleTicket t3 = new SaleTicket();
		SaleTicket t4= new SaleTicket();
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}</span>

    创建四个线程。会创建400张票。不合适,不建议票变成静态的,所以如何共享这100张票。需要将资源和线程分离。
    到api文档中查阅了第二种创建线程的方式,实现Runnable接口。

创建线程方式二:实现Runnable接口

    步骤:

    1.定义一个类实现Runnable。
    2.覆盖Runnable接口中的run方法,将线程要运行的任务代码存储到该方法中。
    3.通过Thread类创建线程对象,并将实现了Runnable接口的对象作为Thread类的构造函数的参数进行传递。
    4.调用Thread类的start方法,开启线程。

    改进了的售票示例的代码如下:

class SaleTicket implements Runnable{
	private int tickets = 100;
	public void run(){
		while(true){
			if (tickets > 0){
				System.out.println(Thread.currentThread().getName()+"...."+tickets--);
			}
		}
	}
}
class TicketDemo2{
	public static void main(String[] args){
		//线程任务对象
		SaleTicket t = new SaleTicket();
		
		//创建四个线程。通过Thread类对象。
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

    实现Runnable接口的好处:
    1.避免了继承Thread类的单继承的局限性。
    2.Runnable接口的出现更符合面向对象,将线程任务单独进行对象的封装。
    3.Runnable接口的出现降低了线程对象和线程任务的耦合性。(解耦)

    综上所述,以后创建线程都是用第二种方式。

解决多线程安全问题:同步--synchronized

    上述售票代码的运行,会出现如下的错误情况:

    产生的原因:
    1.线程任务中有处理到共享的数据。
    2.线程任务中有多条对共享数据的操作。一个线程在操作共享数据的过程中,还没有执行完,其他线程参与了运算,造成了数据的错误。

    解决的思想:
    只要保证多条操作共享数据的代码在某一时间段,被一条线程所执行,在执行期间不允许其他线程参与运算。

    咋保证呢?
    java中提供了一个解决方式:同步代码块
    格式:
    synchronized(对象) // 任意对象都可以。这个对象就是锁,也称为监视器。
    {
        需要被同步的代码。
    }

    再一次改进后的售票示例代码如下:

class SaleTicket implements Runnable{
	private int tickets = 100;
	Object obj = new Object();
	public void run(){
		while(true){
			/*在线程任务中,只有操作到共享数据的部分需要使用同步代码块*/
			synchronized(obj){//obj->同一个锁
				if (tickets > 0){
					try{Thread.sleep(10);}catch(InterruptedException e){}//让线程到这里稍微停一下,这样可以看到cup切换线程的过程。
					System.out.println(Thread.currentThread().getName()+"...."+tickets--);
				}
			}
		}
	}
}
class TicketDemo3{
	public static void main(String[] args){
		SaleTicket t = new SaleTicket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		t1.start();		
		t2.start();		
		t3.start();		
		t4.start();		
	}
}

    运行结果就正确了:

    同步在目前情况下保证了一次只能有一个线程在执行。其他线程进不来。(火车上的厕所)。这就是同步的锁机制

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

    弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁

    有可能出现这样一种情况:
    多线程安全问题出现后,加入了同步机制,没有想到,安全问题依旧!咋办?

    这时,肯定是同步出了问题。

    只要遵守了同步的前提,就可以解决。

    同步的前提
    多个线程在同步中必须使用同一个锁。这才是对多个线程同步。也就是说,必须是多个线程在同一个锁上处理同一个共享数据。

    练习:

    两个储户,到同一个银行存钱,每个人存了3次,一次100元。
    1.描述银行。
    2.描述储户任务。

    分析多线程是否存在安全隐患。
    1.线程任务中是否有共享的数据。
    2.是否有多条操作共享数据的代码。

    发现该程序中是有安全隐患的:

    加上同步以后,问题得以解决:

class Bank{
	private int sum;
	private Object obj = new Object();
	public void add(int n){
		synchronized(obj){
			sum = sum + n;
			System.out.println("sum = "+sum);
		}
		
	}
}
class Customer implements Runnable{
	private Bank b = new Bank();
	public void run(){
		for (int x = 0; x < 3; x++){
			b.add(100);
		}
	}
}
class ThreadTest{
	public static void main(String[] args){
		//1.创建任务对象
		Customer c = new Customer();
		
		//2.创建线程对象
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		
		//3.开启线程
		t1.start();
		t2.start();
	}
}

    运行结果:

匿名线程对象示例

    实际中创建线程,可以使用匿名对象的方式,代码如下:

class ThreadTest{
	public static void main(String[] args)throws InterruptedException{
		new Thread(){
			public void run(){
				for (int x = 1; x <= 50; x++)
					System.out.println(Thread.currentThread().getName()+"...x = "+x);
			}
		}.start();
		
		Runnable r = new Runnable(){
			public void run(){
				for (int x = 1; x <= 50; x++)
					System.out.println(Thread.currentThread().getName()+"y = "+x);
			}
		};
		//new Thread(r).start();
		Thread t = new Thread(r);
		t.start();
		t.join();
		
		for (int x = 1; x <= 50; x++)
			System.out.println(Thread.currentThread().getName()+"z = "+x);
		
		//-----------如果错误,错误发生在哪一行---------
		class Test implements Runnable{
			public void run(Thread t){
				
			}
		}
		//-->错误在第一行,应该被abstract修饰。
		//-->一个类如果实现了接口,但是接口的抽象方法没有全被覆盖,该类应该是抽象类。
		
		
		//------------------结果是什么?-------------------
		new Thread(new Runnable(){
			public void run(){
				System.out.println("runnable run");
			}
		}){
			public void run(){
				System.out.println("subThread run");
			}
		}.start();
		//-->以子类为主。打印:subThread run
	}
}

同步函数

    同步函数其实就是在函数上加上了同步关键字进行修饰。

    同步的表现形式有两种:

    1.同步代码块(明锁)

    2.同步函数(this锁)。

    同步函数使用的锁是什么呢?
    函数需要被对象调用,哪个对象不确定,但是都用this来表示。同步函数使用的锁就是this

class Bank{
	private int sum;
	public synchronized void add(int n){
		sum += n;
		try{Thread.sleep(10);}catch(Exception e){}
		System.out.println("sum = "+sum);
	}
}
class Customer implements Runnable{
	private Bank b = new Bank();
	public void run(){
		for(int x = 0; x < 3; x++){
			b.add(100);
		}
	}
}
class BankDemo{
	public static void main(String[] args){
		Customer c = new Customer();
		Thread t1 = new Thread(c);
		Thread t2 = new Thread(c);
		t1.start();
		t2.start();
	}
}

    如何验证同步函数使用的锁就是this呢?

    验证需求:
    启动两个线程。一个线程负责执行同步代码块(使用明锁)。另一个线程使用同步函数(使用this锁)。两个线程执行的任务是一样的,都是卖票。如果他们没有使用相同的锁,说明他们没有同步,会出现数据错误。

    怎么让一个线程一直在同步代码块中,一个线程在同步函数中呢?可以通过切换的方式。设置flag标记位

    我们还以卖票示例,实验结果发生错误:

    而如果将同步代码块中的obj锁改成this锁,结果就是正确的,说明此时两个线程是同一个锁。这就验证了同步函数使用的是this锁。正确代码如下:

class SaleTicket implements Runnable{
	private int tickets = 100;
	//定义一个boolean标记。
	boolean flag = true;
	Object obj = new Object();
	public void run(){
		if (flag)
			while(true){
				synchronized(this){//obj-->this
					if (tickets > 0){
						try{Thread.sleep(10);}catch(InterruptedException e){}
						System.out.println(Thread.currentThread().getName()+"...code..."+tickets--);
					}
				}
			}
		else
			while(true)
				sale();
	}
	public synchronized void sale(){
		if (tickets > 0){
			try{Thread.sleep(10);}catch(InterruptedException e){}
			System.out.println(Thread.currentThread().getName()+"...func..."+tickets--);
		}
	}
}
class ThisLockDemo{
	public static void main(String[] args) throws InterruptedException{
		SaleTicket s = new SaleTicket();
		Thread t1 = new Thread(s);
		Thread t2 = new Thread(s);
		t1.start();
		Thread.sleep(10);
		s.flag = false;
		t2.start();
	}
}

    运行结果:

    那如果同步函数被static修饰呢?

    注意:字节码文件进入内存后,除了在方法区中进行分布以外,还在堆中生成了一个自己的对象。比如Demo.class字节码文件对象。每个字节码文件对象在堆内存中都是唯一的。也就是说,类进入内存后就有一个对象,这个对象就是字节码文件对象。后期根据new产生的对象都是类的实例,都是根据那个唯一的字节码文件对象在堆内存中创建的。后面说到有关java的反射技术时,会详细介绍。

    static方法随着类加载,这时不一定有该类的对象。但是一定有一个该类的字节码文件对象。这个对象简单的表示方式就是 类名.class (java.lang.Class类)

    所以,被static修饰的同步函数的锁就是类名.class对象。

class SaleTicket implements Runnable{
	private static int tickets = 100;
	boolean flag = true;
	public void run(){
		if (flag)
			while(true){
				synchronized(SaleTicket.class){
					if (tickets > 0){
						try{Thread.sleep(10);}catch(InterruptedException e){}
						System.out.println(Thread.currentThread().getName()+"...code..."+tickets--);
					}
				}
			}
		else
			while(true)
				sale();
	}
	public static synchronized void sale(){
		if(tickets > 0){
			try{Thread.sleep(10);}catch(InterruptedException e){}
			System.out.println(Thread.currentThread().getName()+"...func..."+tickets--);
		}
	}
}
class StaticLockDemo{
	public static void main(String[] args) throws InterruptedException{
		SaleTicket t = new SaleTicket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		Thread.sleep(10);
		t.flag = false;
		t2.start();
	}
}

    同步函数和同步代码块有什么区别呢?

    同步代码块使用的是任意的对象作为锁。
    同步函数只能使用this作为锁。
    如果说,一个类中只需要一个锁,这时可以考虑同步函数,使用this,写法简单。
    但是,一个类中如果需要多个锁,还有多个类中使用同一个锁,这时只能使用同步代码块。

    建议使用同步代码块。

单例模式的并发访问

    饿汉式。相对于多线程并发,比较安全!因为虽然有共享数据,但是没有对共享数据有多条操作。

class Single{
	private static final Single SINGLE_INSTANCE = new Single();
	private Single(){}
	public static Single getInstance(){
		//这里如果有判断语句,注意线程安全。
		return SINGLE_INSTANCE;
	}
}

    懒汉式。延迟加载模式。在多线程并发访问时,会出现线程安全问题。
    加了同步就可以解决。无论是同步函数,还是同步代码块都行。
    但是,效率低了。怎么解决效率问题呢?
    可以通过对单例对象的双重if判断的形式解决!

class Single1{
	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;
	}
}
/*单例延迟加载模式,并且多线程安全,解决效率问题。*/
class Single2{
	private static final Single s = null;
	private Single(){}
	public static Single getInstance(){
		if (s == null){
			lock.lock();
			try{
				if (s == null)
					s = new Single();
			}finally{
				lock.unlock();
			}
		}
		return s;
	}
}
class Demo implements Runnable{
	public void run(){
		Single.getInstance();
	}
}
class ThreadSingleTest{
	public static void main(String[] args){
		
	}
}

    面试:

    延迟加载单例模式-->函数上加同步解决线程安全问题-->使用的锁是类名.class,字节码对象,比如Single.class-->双重判断解决线程并发访问的效率问题。

死锁

    场景一:同步嵌套。
    场景二:所有线程全部冻结,wait().
    代码示例:

class SaleTicket implements Runnable{
	private int tickets = 100;
	boolean flag = true;
	Object obj = new Object();
	public void run(){
		if (flag){
			while(true){
				synchronized (obj){//obj lock
					sale();
				}
			}
		}
		else
			while(true)
				sale();
	}
	public synchronized void sale(){//this lock
		synchronized(obj){
			if (tickets > 0){
				try{Thread.sleep(10);}catch(InterruptedException e){}
				System.out.println(Thread.currentThread().getName()+"...func..."+tickets--);
			}
		}
	}
}
class DeadLockDemo{
	public static void main(String[] args)throws InterruptedException{
		SaleTicket t = new SaleTicket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start();
		Thread.sleep(10);
		t.flag = false;
		t2.start();
	}
}
/*---------------------------------------------------------*/
class Task implements Runnable{
	private boolean flag;
	public Task(boolean flag){
		this.flag = flag;
	}
	public void run(){
		if (flag){
			while (true){
				synchronized(MyLock.LOCKA){
					System.out.println("if......locka");
					synchronized(MyLock.LOCKB){
						System.out.println("if......lockb");
					}
				}
			}
		}
		else{
			while(true){
				synchronized(MyLock.LOCKB){
					System.out.println("else......lockb");
					synchronized(MyLock.LOCKA){
						System.out.println("else......locka");
					}
				}
			}
		}
	}
}
class MyLock{
	public static final Object LOCKA = new Object();
	public static final Object LOCKB = new Object();
}
class DeadLockTest{
	public static void main(String[] args){
		Task t1 = new Task(true);
		Task t2 = new Task(false);
		new Thread(t1).start();
		new Thread(t2).start();
	}
}

多线程间的通信:生产者消费者示例

    现实中,我们经常遇到多个线程都在处理同一个资源,但是处理的任务却不一样。

    这里典型的例子就是生产者消费者问题。

    我们先来看单生产者单消费者问题:

    这里程序会出现还没生产就消费的问题,于是在代码中加了同步得以解决。代码如下:

//描述资源
class Res{
	private String name;
	private int count = 1;
	
	//提供一个给商品赋值的方法
	public synchronized void set(String name){//加了同步之后,不会出现还没生产就消费的情况了。
		this.name = name + "--" + count;
		count++;
		System.out.println(Thread.currentThread().getName()+"....生产者...."+this.name);
	}
	
	//提供一个获取商品的方法
	public synchronized void get(){
		System.out.println(Thread.currentThread().getName()+"......消费者......"+this.name);
	}
}

//生产者
class Producer implements Runnable{
	private Res r;
	Producer(Res r){
		this.r = r;
	}
	public void run(){
		while(true)
			r.set("面包");
	}
}

//消费者
class Consumer implements Runnable{
	private Res r;
	Consumer(Res r){
		this.r = r;
	}
	public void run(){
		while(true)
			r.get();
	}
}

class ProducerConsumerDemo{
	public static void main(String[] args){
		//创建资源
		Res r = new Res();
		
		//创建两个任务
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		
		//创建线程
		Thread t1 = new Thread(pro);		
		Thread t2 = new Thread(con);	
		
		//开启线程
		t1.start();
		t2.start();
	}
}

    但是出现了连续的生产没有消费的情况,和需求生产一个就消费一个的情况不符。

    如何实现生产一个就消费一个呢?

等待唤醒机制

    Object监视器方法:
    wait():该方法可以让线程处于冻结状态,并将线程临时存储到线程池中。
    notify():唤醒指定线程池中的任意一个线程。(具体唤醒哪个线程是随机的,允许空唤醒)
    notifyAll():唤醒指定线程池中的所有线程。

    注意:只要使用等待唤醒机制,都应该使用循环判断

class Res{
	private String name;
	private int count = 1;
	private boolean flag;//定义标记(java中默认值是false)
	
	//提供了给商品赋值的方法。(该同步函数用的锁或者说监视器是this)
	public synchronized void set(String name){
		if (flag)//判断标记为true,执行wait等待;标记为false,就生产。
			try{this.wait();}catch(InterruptedException e){}
		this.name = name + "--" + count;
		count++;
		System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
		flag = true;//生产完毕,将标记改为true。
		this.notify();//唤醒消费者。
	}
	
	//提供一个获取商品的方法。
	public synchronized void get(){
		if (!flag)
			try{this.wait();}catch(InterruptedException e){}
		System.out.println(Thread.currentThread().getName()+".....消费者....."+this.name);
		flag = false;//将标记改为false。
		this.notify();//唤醒生产者。
	}
}
class Producer implements Runnable{
	private Res s;
	Producer(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.set("面包");
	}
}
class Consumer implements Runnable{
	private Res s;
	Consumer(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.get();
	}
}
class ProducerConsumerDemo2{
	public static void main(String[] args){
		Res r = new Res();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(con);
		t1.start();
		t2.start();
	}
}

    使用了等待唤醒机制后,单生产者单消费者问题得以解决。运行结果如下 :

    wait()、notify()、notifyAll()方法必须使用在同步代码块或者同步函数中,因为它们是用来操作同一同步锁上的线程的状态的(即操作同一个监视器对象上线程状态的一组方法)。必须要明确到底操作的是哪个锁上的线程。在使用这些方法时,必须标识它们所属于的锁。标识方式就是锁对象.wait(); 锁对象.notify(); 锁对象.notifyAll();相同锁的notify(),可以获取相同锁的wait()。

    wait(),notify(),notifyAll()用来操作线程为什么定义在了Object类中?
    1.这些方法存在于同步(synchronized)中。
    2.使用这些方法时必须要标识所属的同步的锁。
    3.锁可以是任意对象,所以任意对象调用的方法一定定义在Object类中。
    其实这些方法是监视器方法,监视器就是锁,锁可以是任意对象,任意对象调用的方式一定定义在Object中。

    备注:
    synchronized 同步函数或同步代码块 可持任意对象作为同步锁 同一锁对象 同一对象监视器 同一组监视器方法 同一线程池 同一等待集 自动释放锁的功能
    同一锁对象所对应的wait,notify,notifyAll监视器方法操作该锁对象上的若干线程
    使用wait,notify,notifyAll监视器方法必须标识所属的同步锁对象,必须用在synchronized中
    wait释放cpu执行权,释放锁。
    sleep释放cpu执行权,不释放锁。

多生产者多消费者问题

    接下来,我们来看多生产多消费的例子。

    如果我们仅仅是多开一对线程,代码会出现以下两个问题:

    问题1:重复生产、重复消费
    原因:经过复杂(冻结、临时阻塞、运行)的分析,发现被唤醒的线程没有判断标记就开始工作(生产or消费)了。导致了重复的生产和消费的发生。因为使用的if判断标记,所以从wait处唤醒后直接就向下执行代码。
    解决:被唤醒的线程必须判断标记。使用while循环搞定。

    问题2:死锁了。所有的线程都处于冻结状态。
    原因:本方线程在唤醒时,又一次唤醒了本方线程。而本方线程循环判断标记,又继续wait,而导致所有的线程都wait了。
    解决:希望本方如果唤醒了对方线程,就可以解决了。可以使用notifyAll()方法。

    疑问:那不是全唤醒了吗?
    回答:是的。既有本方,又有对方。但是本方醒后,会判断标记继续wait。这样对方就有线程可以执行了。

    代码如下:

class Res{
	private String name;
	private int count = 1;
	private boolean flag;
	public synchronized void set(String name){
		while(flag)
			try{this.wait();}catch(InterruptedException e){}//每次醒来都应该再次判断标记。所以用while,安全。
		this.name = name + "--" + count;
		count++;
		System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
		flag = true;
		this.notifyAll();//唤醒所有等待线程(包括本方线程)
	}
	public synchronized void get(){
		while(!flag)
			try{this.wait();}catch(InterruptedException e){}
		System.out.println(Thread.currentThread().getName()+".....消费者....."+this.name);
		flag = false;
		this.notifyAll();
	}
}
class Producer implements Runnable{
	private Res s;
	Producer(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.set("面包");
	}
}
class Consumer implements Runnable{
	private Res s;
	Consumer(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.get();
	}
}
class ProducerConsumerDemo3{
	public static void main(String[] args){
		Res r = new Res();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		Thread t0 = new Thread(pro);
		Thread t1= new Thread(pro);
		Thread t2 = new Thread(con);
		Thread t3 = new Thread(con);
		t0.start();
		t1.start();
		t2.start();
		t3.start();
	}
}

    该程序已经实现了多生产多消费。但是有些小问题,效率有点低。因为notifyAll也唤醒了本方,做了不必要的标记判断。而且唤醒了对方全部,也不太合适。如何解决效率问题呢?

JDK1.5: Lock接口、Condition接口

    解决多生产多消费的效率问题。使用了JDK.5 java.util.concurrent.locks包中的对象。

    Lock接口:它的出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器

    lock():获取锁。
    unlock():
释放锁,通常需要定义到finally代码块中。

    同步代码块或者同步函数的锁操作是隐式的。
    JDK1.5 Lock接口,按照面向对象的思想,将同步和锁单独封装成了一个对象。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。

    Lock接口就是同步的替代。将线程中的同步更换为Lock接口的形式。

    注意:不使用synchronized块结构锁就失去了使用synchronized方法和语句时会出现的锁自动释放功能,需使用finally代码块确保执行unlock释放锁

    替换完运行失败了。无效的监视器状态异常:IllegalMonitorStateException。这是为什么?
    因为wait没有了同步区域,没有了所属的同步锁。那么就不能够使用绑定在同步锁对象上的监视器方法(wait,notify,notifyAll)。同步升级了。其中锁已经不再是任意对象,而是Lock类型的对象。那么和任意对象(同步锁)绑定的监视器方法(监视该锁上线程的状态的方法),是不是也升级了,有专门和Lock类型锁绑定的监视器方法呢?答案是有的!

    查阅api:
    Condition接口:它的出现替代了Object中的wait,notify,notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以和任意锁进行组合。

    await():wait()
    signal():notify()
    signalAll():notifyAll()

    Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用

    但是,问题依旧,一样唤醒了本方,效率仍旧低!

    Condition将Object监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用。Lock可以支持多个相关的Condition对象。

    以前监视器方法封装到每一个对象(Object)中。现在将监视器方法封装到了Condition对象中。
    方法名为: await signal signalAll

    那监视器对象Condition如何和Lock绑定呢?
    可以通过Lock接口的newCondition()方法完成。它返回绑定到此Lock实例的新Condition实例。

    JDK1.4:
    监视器方法-->Object同步锁对象。
    监视器与锁绑定,锁也就是监视器,一个锁上只能有一组监视器。
    要想一组监视生产者,一组监视消费者,从而让生产者等待的同时唤醒消费者,那么需要将两个锁嵌套,才可以两组监视器,但是这样容易发生死锁。所以我们使用了while、notifyAll的组合,但是效率偏低。

    JDK1.5:
    监视器方法-->Condition对象-->Lock对象。
    监视器方法封装在Condition对象中,一个Lcok锁对象可以绑定多个监视器对象。
    这样就可以一个监视器对象监视生产者,一个监视器对象监视消费者,生产的await与消费的signal搭配使用,就相当于同一个锁上有了两个线程池。这样就实现了本方只唤醒对方中的一个。

    使用JDK1.5 Lock接口改善效率问题,多生产者多消费者完整示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

class Res{
	private String name;
	private int count = 1;
	private boolean flag;
	
	private Lock lock = new ReentrantLock();//创建一个锁对象
	
	//private Condition con = lock.newCondition();//通过已有的锁对象获取该锁上的监视器对象,使得监视器和锁绑定。
	
	//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
	private Condition producer_con = lock.newCondition();
	private Condition consumer_con = lock.newCondition();
	
	public void set(String name){
		lock.lock();
		try{
			while(flag)
				try{producer_con.await();}catch(InterruptedException e){}//只要醒了,就判断,应该使用循环判断。
			this.name = name +"--"+ count;
			count++;
			System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name);
			flag = true;
			//this.notifyAll();
			//con.signalAll();
			consumer_con.signal();//生产完毕,应该唤醒一个消费者来消费。
		}finally{
			lock.unlock();
		}
	}
	public void get(){
		lock.lock();
		try{
			while(!flag)
				try{consumer_con.await();}catch(InterruptedException e){}
			System.out.println(Thread.currentThread().getName()+".....消费者5.0....."+this.name);
			flag = false;
			//this.notifyAll();
			//con.signalAll();
			producer_con.signal();//消费完后,应该唤醒一个生产者。
		}finally{
			lock.unlock();
		}
	}
}
class Producer implements Runnable{
	private Res s;
	Producer(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.set("面包");
	}
}
class Consumer implements Runnable{
	private Res s;
	Consumer(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.get();
	}
}
class NewProducerConsumerDemo{
	public static void main(String[] args){
		Res r = new Res();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		Thread t0 = new Thread(pro);
		Thread t1= new Thread(pro);
		Thread t2 = new Thread(con);
		Thread t3 = new Thread(con);
		t0.start();
		t1.start();
		t2.start();
		t3.start();
	}
}

    JDK1.5的API文档中,Condition接口示例:

class BoundedBuffer {
	final Lock lock = new ReentrantLock();
	final Condition notFull  = lock.newCondition(); 
	final Condition notEmpty = lock.newCondition(); 

	final Object[] items = new Object[100];
	int putptr, takeptr, count;

	public void put(Object x) throws InterruptedException {
		lock.lock();
		try {
			while (count == items.length) 
				notFull.await();
			items[putptr] = x; 
			if (++putptr == items.length) putptr = 0;
			++count;
			notEmpty.signal();
		} finally {
			lock.unlock();
		}
	}

	public Object take() throws InterruptedException {
		lock.lock();
		try {
			while (count == 0) 
				notEmpty.await();
			Object x = items[takeptr]; 
			if (++takeptr == items.length) takeptr = 0;
			--count;
			notFull.signal();
			return x;
		} finally {
			lock.unlock();
		}
	} 
}

    练习:

    1.搞定妖的问题。
      分析:1.共享数据 2.线程任务中有多条操作共享数据的代码。
      加了同步,问题依旧。看同步的前提!多个线程、同一个锁。将输入输出都加了同步,问题解决

    2.name和sex是私有的。需要在Res类中对外提供访问name和sex的方法。这个可以参照生产者消费者ProducerConsumerDemo.java。已解决。

    3.实现间隔输出,使用等待唤醒机制。ProducerConsumerDemo.java
      一般情况下需要判断条件。

import java.util.concurrent.locks.*;

/*实在找不到合适的锁,就自己定义一个*/
class MyLock{	//自定义锁
	public static final Object obj = new Object();
}

class Res{
	private String name;
	private String sex;
	private boolean flag;
	private Lock lock = new ReentrantLock();
	private Condition con = lock.newCondition();
	public void set(String name, String sex){
		lock.lock();
		try{
			while(flag)
				try{con.await();}catch(InterruptedException e){}
			this.name = name;
			this.sex = sex;
			flag = true;
			con.signal();
		}finally{
			lock.unlock();
		}
	}
	public void get(){
		lock.lock();
		try{
			while(!flag)
				try{con.await();}catch(InterruptedException e){}
			System.out.println(this.name+"......"+this.sex);
			flag = false;
			con.signal();
		}finally{
			lock.unlock();
		}
	}
}
class Input implements Runnable{
	private Res s;
	Input(Res s){
		this.s = s;
	}
	public void run(){
		int x = 0;
		while(true){
			if (x == 0)
				s.set("张三","男男男男男男");
			else
				s.set("rose","woman");
			x = (x+1)%2;
		}
	}
}
class Output implements Runnable{
	private Res s;
	Output(Res s){
		this.s = s;
	}
	public void run(){
		while(true)
			s.get();
	}
}
class Test{
	public static void main(String[] args){
		Res r = new Res();
		Input in = new Input(r);
		Output out = new Output(r);
		Thread t0 = new Thread(in);
		Thread t1 = new Thread(out);
		t0.start();
		t1.start();
	}
}

wait()和sleep()区别

    相同:可以让线程处于冻结状态。
    不同:
    1.wait(): 可以指定时间,也可以不指定。
      sleep():必须指定时间。
    2.wait(): 释放CPU资源,释放锁。
      sleep():释放CPU资源,不释放锁。

    同步里面只能有一个线程。但是同步中如果有wait(),会出现多线程情况,但是不用担心数据错误的问题。是因为,在同步中的临时阻塞状态的线程要想运行,必须要持有锁。只有持有锁的那个线程具有执行权。执行完再把锁放掉,其他线程才有资格执行。

    所以记住,在同步之中,假设有n个线程从冻结状态恢复到了临时阻塞状态,即具备了执行资格,此时如果锁没有被任何线程持有,并且CPU切到这n个线程其中的一个上,那么这个线程同时具备执行权和锁。只有拿到锁的这个线程才能运行。 所以即使都醒了,也不怕,因为任意时刻只有一个线程可以持有锁,持有锁的线程才能执行。

    同步中,具备执行资格的活着的线程可以有多个,但是真正具备执行权的运行的线程只有一个。谁持有着锁,谁就运行。

synchronized(obj){
	obj.wait();//t0, t1, t2,...,tn争夺执行权,获得执行权的同时获得被t3释放的锁
	code...
}
synchronized(obj){
	obj.notifyAll();//t3
}//t3释放锁

异常在多线程中的体现

    异常会提示它发生在哪个线程上。

    异常会结束线程任务,也就是说可以结束所在线程。

class Demo implements Runnable{
	public void run(){
		System.out.println(4/0);
	}
}
class ThreadExceptionDemo{
	public static void main(String[] args)throws Exception{
		new Thread(new Demo()).start();
		Thread.sleep(10);
		int[] arr = new int[3];
		System.out.println(arr[2]);
		System.out.println("over");
	}
}

    运行结果:

停止线程方式

    方法一:定义循环结束标记(变量)
    原理:让run方法结束。
    线程任务通常都有循环。因为开启线程就是为了执行需要一些时间的代码。不让任务A苦苦等待任务B的完成才执行。
    只要控制住循环,就可以结束run方法,就可以停止线程。

class StopThread implements Runnable{
	private boolean flag = true;
	public void run(){
		while(flag){
			System.out.println(Thread.currentThread().getName()+"......");
		}
	}
	public void setFlag(){
		this.flag = false;
	}
}
class StopThreadDemo{
	public static void main(String[] args){
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		int num = 1;
		for(;;){
			if (++num == 50){
				st.setFlag();
				break;
			}
			System.out.println(Thread.currentThread().getName()+"............"+num);
		}
		System.out.println("over");
	}
}

    但是,第一种方式会出现死锁的情况,如果线程处于冻结状态,就无法读到定义的标记,也就无法在想要的时刻实现停止线程。如下所示:

class StopThread implements Runnable{
	private boolean flag = true;
	public synchronized void run(){
		while(flag){
			try{
				wait();//t0  t1
			}catch(InterruptedException e){
				System.out.println(Thread.currentThread().getName()+"......"+e);
			}
			System.out.println(Thread.currentThread().getName()+".....++++++");
		}
	}
	public void setFlag(){
		this.flag = false;
	}
}
class StopThreadDemo{
	public static void main(String[] args){
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		int num = 1;
		for(;;){
			if (++num == 50){
				st.setFlag();
				break;
			}
			System.out.println(Thread.currentThread().getName()+"............"+num);
		}
		System.out.println("over");
	}
}

    方法二:使用interrupt(中断)方法
    interrupt():中断线程,强制结束线程的冻结状态,并抛出异常。
    将线程从冻结状态强制恢复到临时阻塞或运行状态,让线程具备cpu的执行资格,但是强制动作会发生InterruptedException异常,记得要处理。

class StopThread implements Runnable{
	private boolean flag = true;
	public  synchronized void run(){
		while(flag){
			try{
				this.wait();
			}catch(InterruptedException e){
				System.out.println(Thread.currentThread().getName()+"................"+e.toString());
				flag = false;
			}
			System.out.println(Thread.currentThread().getName()+"......hello");
		}
	}
}
class StopThreadDemo{
	public static void main(String[] args)throws InterruptedException {
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		Thread.sleep(10);
		for(int x = 0; x <= 50; x++){
			if (x == 40){
				t1.interrupt();//强制唤醒线程,并抛出InterruptedException异常。
				t2.interrupt();
			}
			System.out.println(Thread.currentThread().getName()+"......"+x);
		}
		System.out.println("main over");
	}
}

    stop()方法已经过时,不再使用。

守护进程

    守护进程:即后台进程,当所有前台进程结束,后台进程无论是否执行完,随之结束。当正在运行的线程都是守护进程时,jvm退出。
    setDaemon():该方法必须在启动线程前调用。

    应用:比如一个输入线程,一个输出线程,可以将输出线程置为守护线程。只要没有输入了,输出线程就自动结束。

class StopThread implements Runnable{
	private boolean flag = true;
	public synchronized void run(){
		while(flag){
			try{
				this.wait();
			}catch(InterruptedException e){
				System.out.println(Thread.currentThread().getName()+"............"+e.toString());
				flag = false;
			}
			System.out.println(Thread.currentThread().getName()+"......hello");
		}
	}
}
class StopThreadDemo2{
	public static void main(String[ ] args){
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.setDaemon(true);//将t2设置为守护进程。
		t2.start();
		for(int x = 0; x <= 50; x++){
			if (x == 40){
				t1.interrupt();
			}
			System.out.println(Thread.currentThread().getName()+"..."+x);
		}
		System.out.println(Thread.currentThread().getName()+" over");
	}
}

Thread类的一些方法

    join():等待该线程终止。临时加入一个线程运算时,可以使用join方法。

class Demo implements Runnable{
	public void run(){
		for (int x = 1; x <= 40; x++)
			System.out.println(Thread.currentThread().getName()+"......"+x);
	}
}
class JoinDemo{
	public static void main(String[] args)throws InterruptedException{
		Demo d = new Demo();
		Thread t0 = new Thread(d);
		Thread t1 = new Thread(d);
		t0.start();
		
		//t0.join();//t0线程申请加入进来运行,当前主线程释放执行权和执行资格,处于冻结状态,等待t0线程终止,主线程再醒过来执行。
		//执行这句话的当前线程冻结,抛出执行权。(本例也即主线程冻结)
		
		t1.start();
		
		t0.join();
		//执行到这句话的主线程释放执行权和执行资格,处于冻结状态。
		//临时阻塞的所有线程t0、t1争夺该执行权。
		//冻结的主线程等到调用join的t0线程终止才被唤醒,重新获得执行资格抢夺执行权。
		
		for (int x = 1; x <= 40; x++)
			System.out.println(Thread.currentThread().getName()+"......"+x);
	}
}

    toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

    setPriority():设置线程优先级(1-10),优先级越大,当前线程获取cpu执行权的几率越高。默认优先级:5

    yield():暂停当前正在执行的线程对象。

    线程组ThreadGroup:也就是一个集合,后面会讲到。操作线程组,也就是同时操作线程组中的所有线程。

class Demo implements Runnable{
	public void run(){
		for (int x = 1; x <= 40; x++){
			System.out.println(Thread.currentThread().toString()+"......"+x);
			Thread.yield();//暂停当前正在执行的线程对象,释放执行权。
		}
	}
}
class PriorityDemo{
	public static void main(String[] args)throws InterruptedException{
		Demo d = new Demo();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		t1.start();
		t2.start();
		//t1.join();//等待该线程终止。
		Thread.sleep(10);
		t1.setPriority(Thread.MAX_PRIORITY);//最大优先级:10
		t2.setPriority(Thread.MIN_PRIORITY);//最小优先级:1
		for (int x = 1; x <= 40; x++)
			System.out.println(Thread.currentThread().toString()+"......"+x);
	}
}

多线程总结

1,进程和线程的概念。
 |--进程:
 |--线程:

2,jvm中的多线程体现。
 |--主线程,垃圾回收线程,自定义线程。以及他们运行的代码的位置。

3,什么时候使用多线程,多线程的好处是什么?创建线程的目的?
 |--当需要多部分代码同时执行的时候,可以使用。

4,创建线程的两种方式。★★★★★
 |--继承Thread
  |--步骤
 |--实现Runnable
  |--步骤
 |--两种方式的区别?

5,线程的5种状态。
 对于执行资格和执行权在状态中的具体特点。
 |--被创建:
 |--运行:
 |--冻结:
 |--临时阻塞:
 |--消亡:

6,线程的安全问题。★★★★★
 |--安全问题的原因:
 |--解决的思想:
 |--解决的体现:synchronized
 |--同步的前提:但是加上同步还出现安全问题,就需要用前提来思考。
 |--同步的两种表现方法和区别:
 |--同步的好处和弊端:
 |--单例的懒汉式。
 |--死锁。

7,线程间的通信。等待/唤醒机制。
 |--概念:多个线程,不同任务,处理同一资源。
 |--等待唤醒机制。使用了锁上的 wait notify notifyAll.  ★★★★★
 |--生产者/消费者的问题。并多生产和多消费的问题。  while判断标记。用notifyAll唤醒对方。 ★★★★★
 |--JDK1.5以后出现了更好的方案,★★★
  Lock接口替代了synchronized 
  Condition接口替代了Object中的监视方法,并将监视器方法封装成了Condition
  和以前不同的是,以前一个锁上只能有一组监视器方法。现在,一个Lock锁上可以多组监视器方法对象。
  可以实现一组负责生产者,一组负责消费者。
 |--wait和sleep的区别。★★★★★

8,停止线程的方式。
 |--原理:
 |--表现:--中断。

9,线程常见的一些方法。
 |--setDaemon()
 |--join();
 |--优先级
 |--yield();
 |--在开发时,可以使用匿名内部类来完成局部的路径开辟。

 

好了,Java多线程的内容就这么多,接下来将介绍Java中常用的一些API。

有任何问题请和我联系,共同进步:lichunchun4.0@gmail.com

转载请声明出处:http://www.voidcn.com/article/p-zaaattae-rz.html

相关文章
相关标签/搜索