曾经制霸的秘密——java多线程(附实况练习)

前言

1995年,在java诞生之初,诸如James Gosling等java的主要设计者,非常明智的选择让java内置支持“多线程”,这使得java相比同时期的其它编程语言,有着非常明显的优势。

我今天的讨论,也就由此拉开序幕:
(声明:只是学术研究和讨论,有任何疑问欢迎指出!)

java线程的概念

官方给出的解释是:线程是彼此互相独立的,能独立运行的子任务。
这句话一点没错,只是太过官方,我们来具体一点!
什么是“进程”:好比你打开了电脑,打开QQ,就是一个进程,打开idea,这也是一个进程。进程之间往往会制造出“同步”的假象(当然,这一点在线程中有过之而无不及)。
什么是“线程”:你打开QQ后,既想发消息,又想听音乐。QQ这个进程为你运行了两个“线程”,同样的,这两者同步的只是电脑给人的假象,这一切,还要“归罪”到强大的CPU上。(开个玩笑)

分时
通俗来讲,就是可以同一时间执行多个程序的操作系统,在自己电脑上,可以一边听歌,一边看视频,一边浏览网页。但实际上,CPU只是将时间切割成时间片,然后将这些时间片分配给这些程序,这样一个个的程序在极短时间内获得时间片从而运行,达到“同时”的效果。

学线程,你需要知道什么

用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。
很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。
多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
并行与并发:
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

并发与并行
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:

void transferMoney(User from, User to, float amount){
  to.setMoney(to.getBalance() + amount);
  from.setMoney(from.getBalance() - amount);
}

同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

进入主题:线程的状态

每个线程的教学都会讲这个部分,in fact,他的确是线程不可分割的一部分,但是,我并不打算在这里给你们消磨时间的机会。
曾经制霸的秘密——java多线程(附实况练习)
各种状态一目了然,值得一提的是"blocked"这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况

调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

小叙——继承?实现?

1.扩展java.lang.Thread类
这里继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用Thread.(笔者推荐使用Runable,后头会说明为什么)。下面来看一个简单的实例
[java] view plain copy

package com.multithread.learning;  
/** 
 *@functon 多线程学习 
 *@author 林炳文 
 *@time 2015.3.9 
 */  
class Thread1 extends Thread{  
    private String name;  
    public Thread1(String name) {  
       this.name=name;  
    }  
    public void run() {  
        for (int i = 0; i < 5; i++) {  
            System.out.println(name + "运行  :  " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}  
public class Main { 
    public static void main(String[] args) {  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();   
    }    
}  

输出:
A运行  :  0
B运行  :  0
A运行  :  1
A运行  :  2
A运行  :  3
A运行  :  4
B运行  :  1
B运行  :  2
B运行  :  3
B运行  :  4

再运行一下:

A运行  :  0
B运行  :  0
B运行  :  1
B运行  :  2
B运行  :  3
B运行  :  4
A运行  :  1
A运行  :  2
A运行  :  3
A运行  :  4

说明:
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。——start()—>调度线程,让其做准备。

从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。
[java] view plain copy
在CODE上查看代码片派生到我的代码片
Thread1 mTh1=new Thread1(“A”);
Thread1 mTh2=mTh1;
mTh1.start();
mTh2.start();

输出:

Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Unknown Source)
    at com.multithread.learning.Main.main(Main.java:31)
A运行  :  0
A运行  :  1
A运行  :  2
A运行  :  3
A运行  :  4

二、实现java.lang.Runnable接口
采用Runnable也是非常常见的一种,我们只需要重写run方法即可。下面也来看个实例。
[java] view plain copy

/** 
 *@functon 多线程学习 
 *@author 林炳文 
 *@time 2015.3.9 
 */  
package com.multithread.runnable;  
class Thread2 implements Runnable{  
    private String name;  
  
    public Thread2(String name) {  
        this.name=name;  
    }  
  
    @Override  
    public void run() {  
          for (int i = 0; i < 5; i++) {  
                System.out.println(name + "运行  :  " + i);  
                try {  
                    Thread.sleep((int) Math.random() * 10);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
          
    }  
      
}  
public class Main {  
  
    public static void main(String[] args) {  
        new Thread(new Thread2("C")).start();  
        new Thread(new Thread2("D")).start();  
    }  
  
}  
输出:
C运行  :  0
D运行  :  0
D运行  :  1
C运行  :  1
D运行  :  2
C运行  :  2
D运行  :  3
C运行  :  3
D运行  :  4
C运行  :  4

说明:
Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
当然,事无绝对,如果在Thread里对如变量使用static,也是可以的。。。

正式见面:线程境界-常用函数说明

线程的各个状态的实际操作方法,才是我们需要讨论的问题,

1.线程的休眠sleep与挂起yield
线程可以使用sleep()让当前线程暂停一会后继续执行,其目的就是给其他线程执行的机会。

说到这,其实有一个地方挺有意思的:
线程的调度分为两种模型:分时调度模型和抢占式调度模型。
所谓分时调度,就是所有县城轮流使用CPU,平均分配时间片;而抢占式,就是靠优先级说话!
而不巧的是,java的线程调度模型是抢占式,

这就意味着:我们需要知道另一个概念:优先级

优先级,分为:MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY(标准)
而优先级不是“先天”的,我们可以通过下面方式来为线程设置优先级:

Thread对象.setPriority(...);

值得注意的是,设置优先级这一步骤,必须要在线程启动(start)前设置完成。

而线程的挂起,不能有用户指定线程暂停多长时间,但可以限制只有同优先级的线程有才机会执行。如下:

public void run(){
	for(int k=0;k<10;k++){
		if(k==5 && Thread.currentThread().getName().equals("test1")){
			Thread.yield();
		}
		System.out.println(Thread.currentThread().getName()+":"+k);
	}
}
public static void main(String[] args){
	Test test1=new Test();
	Test test2=new Test();
	Thread t1=new Thread(test1,"test1");
	Thread t2=new Thread(test2,"test2");
	t1.setPriority(Thread.MAX_PRIORITY);
	t2.setPriority(Thread.MIN_PRIORITY);
	t1.start();
	t2.start();
}

运行以后,我们会发现,事情并没有按我们想象中的来,怎么回事?
仔细看前面我说过的内容,可以发现,倒数第四、五行为两个线程设置了不同的优先级,原因一览无余。

我们可以通过设置相同优先级来达到目的,不过,有没有其他方法呢?

2.线程的联合——join
通常,我们再一个线程中加入join后,次线程会暂停执行,一直等到加入的线程执行完毕后再继续执行。
格式:

第一个线程执行
try{
	Thread.sleep(1000);
	第二个线程的对象.join();
}catch(Exception e){
	e.printStack();
}

3.线程同步——synchronized
许多线程在执行过程中必须考虑与其他线程的数据关系,我们称为“共享数据 或 协调执行状态”,这就需要同步机制。
在java中每个对象都有一把锁与其对应,但java不提供单独的lock与unlock操作,它由高层的结构隐式实现。
就像我们开头讨论的Thread与Runnable,不就是因为“数据共享”的问题吗?

但是,事无绝对,有一点必须注意,你必须清楚的知道哪些数据是共享的。请看下面例子:

public **synchronized** void run(){
	for(int i=0;i<10;i++){
		System.out.println(""+i);
	}
}

这样,你就算运行1000次程序,它的结果也一定是01234567890123456789,而不会发生错乱,因为这里synchronized起到了保护作用。
在可能的情况下,应该把保护范围缩到最小,也就是“使用代码块保护数据”(使用this代表当前对象):

public void run(){
**synchronized(this)**{
	for(int i=0;i<10;i++){
		System.out.println(""+i);
	}
}

小叙收尾——线程实际操做练习

我们定义这样一个场景:
学生钱不够了问父母要,同时停止消费(线程),唤醒充值线程,父母向卡里打钱,同时唤醒学生线程,通知学生已经打过钱,,,等等,(涉及知识点有synchroized,wait,sleep,yield,线程唤醒,锁的恢复,映射…)

package 线程的实例操作;

/**
 *    银行类
 */
class BankCard {
    int sum=0;

    //存款
    //加线程锁,使共享资源
    public synchronized void save(String name,int count){
        //如果存够了足够的钱就不再存了
        while(sum>5000){
            try{
                System.out.println(name+"\t存款,发现钱够了");
                //等待,并且从这里退出push
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        //注意,notifyAll()以后,并没有退出,而是继续执行直到完成
        this.sum+=count;
        System.out.println(name+"\t存入了[¥"+count+"]\t余额[¥"+this.sum+"]");

        //因为我们不确定有没有线程在wait,所以我们既然存了钱,就唤醒孩子线程,让他们准备取款
        notifyAll();
        System.out.println("\t"+name+"告诉孩子存了钱");
    }
    //取款
    public synchronized void cost(String name,int count){
        //如果钱不够了,就不再取款
        while(sum<count){
            try{
                System.out.println(name+"\t取款:等钱花"+count);
                //等待,并从这里退出pop
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        //注意,notifyAll以后,并没有退出,而是继续执行直到完成
        this.sum-=count;
        System.out.println(name+"\t取走了[¥"+count+"]\t余额[¥"+this.sum+"]");

        //因为不确定有没有线程在wait,所以我们既然消费了产品,就唤醒有可能的生产者
        notifyAll();
        System.out.println("\t"+name+"告诉父母取了钱");
    }
}

/**
 *    父母类
 */
class Parent implements Runnable{
    BankCard card=null;   //这里是映射——对象实现从属关系
    String name;
    int count;
    int interval;   //存款时间间隔

    Parent(BankCard card,String name,int count,int interval){
        this.card=card;
        this.count=count;
        this.name=name;
        this.interval=interval;
    }

    public void run(){
        while(true){
            card.save(name,count);
            try{
                Thread.sleep(interval);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

/**
 *    孩子类
 */
class Children implements Runnable{
    BankCard card=null;
    String name;
    int count;
    int interval;
    Children(BankCard card,String name,int count,int interval){
        this.card=card;
        this.count=count;
        this.name=name;
        this.interval=interval;
    }

    public void run(){
        while(true){
            //int count=(int)(Math.random()*degree);
            card.cost(name,count);
            try{
                Thread.sleep(interval);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}


/**
 *   存取款类
 */
public class CheckInOut{
    public static void main(String[] args){
        BankCard card=new BankCard();
        Parent px=new Parent(card,"爸爸",1500,500);
        Parent py=new Parent(card,"妈妈",1000,800);
        Parent pz=new Parent(card,"爷爷",800,1000);
        Children ca=new Children(card,"大女儿",400,600);
        Children cb=new Children(card,"二女儿",300,600);
        Children cc=new Children(card,"三女儿",500,600);
        new Thread(px).start();
        new Thread(py).start();
        new Thread(pz).start();
        new Thread(ca).start();
        new Thread(cb).start();
        new Thread(cc).start();
    }
}

运行一下,你会有额外的体会呦~