曾经制霸的秘密——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,他的确是线程不可分割的一部分,但是,我并不打算在这里给你们消磨时间的机会。
各种状态一目了然,值得一提的是"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();
}
}
运行一下,你会有额外的体会呦~