JavaSE实战——多线程

时间:2021-07-19 18:35:40

转载请声明出处:http://blog.csdn.net/zhongkelee/article/details/45892177

说在前面

    在介绍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");
}
}
    运行结果如下:

JavaSE实战——多线程

    通过实验,会发现每次结果不一定相同,那是因为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();
}
}

    输出为:

JavaSE实战——多线程

    那么,如何开辟一个执行路径呢?
    通过查阅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);
}
}
}

    程序输出:

JavaSE实战——多线程

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

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

    内存图解:

JavaSE实战——多线程

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

多线程的五种状态

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

JavaSE实战——多线程

多线程示例一:售票示例

    卖票的代码需要被多个线程执行,所以要将这些代码定义在线程任务中。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

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

JavaSE实战——多线程

    产生的原因:
    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();
}
}

    运行结果就正确了:

JavaSE实战——多线程

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

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

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

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

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

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

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

    练习:

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

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

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

JavaSE实战——多线程

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

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();
}
}

    运行结果:

JavaSE实战——多线程

匿名线程对象示例

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

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标记位

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

JavaSE实战——多线程

    而如果将同步代码块中的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();
}
}

    运行结果:

JavaSE实战——多线程

    那如果同步函数被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();
}
}

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

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

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

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

JavaSE实战——多线程

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

//描述资源
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();
}
}

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

JavaSE实战——多线程

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

等待唤醒机制

    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();
}
}

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

JavaSE实战——多线程

    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执行权,不释放锁。

多生产者多消费者问题

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

JavaSE实战——多线程

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

    问题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实例。

JavaSE实战——多线程

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

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

JavaSE实战——多线程

    使用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");
}
}

    运行结果:

JavaSE实战——多线程

停止线程方式

    方法一:定义循环结束标记(变量)
    原理:让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://blog.csdn.net/zhongkelee/article/details/45892177