一. 多线程的概述
1.进程
要想了解线程,我们先来说一说进程,进程就是正在运行中的程序,他是一个操作系统中的子程序,他代表着一个应用程序在内存中所开辟的空间,例如qq.exe就是一个进 程。多进程就是在一个操作系统中执行的多个并发的子程序。
2.线程
线程就是进程中负责程序执行的一个控制单元,也叫执行路径,一个进程中可以有多个执行路径,称为多线程,这些线程是并发的,一个进程中至少要有一条线程,如果一条线程都没有程序还怎么运行呢,我们开启多个线程是为了同时运行多个部分的代码,这些线程都有自己单独的运行内容,这个内容我们可以称之为线程要执行的任务,多线程的好处是提高的cpu的利用率。
注意点是我们的线程被cpu执行时,cpu在某一特定时间只能执行一个线程,但是cpu在做不断的切换动作,因为速度太快才被认为是同时执行,这个切换是随机的,如果我们的线程超多,切换的频率就会变慢,我们的电脑就特别卡,这也是多线程的弊端。
在我们启动虚拟机去执行我们的程序时就已经开启了多个线程,例如执行主函数的主线程,回收垃圾的线程等,当我们的程序中有多条线程时,即使我们的主线程退出,即main()方法退出了,也不会影响其他的线程,只有当我们的程序中所有线程都中止后,虚拟机这个进程才会退出。
二. 创建线程
1. 第一种:继承Thread类
1>创建一个类继承Thread类,
2>覆盖Thread类中的run()方法
3>创建Thread类的子类对象
4>调用start()方法开启线程
我们可以理解继承Thread类是为了构建线程要拿到其父类的方法,但是为什么要覆盖掉父类中的run方法呢?这是因为我们创建线程的目的就是为了开启一条执行路径来运行我们指定的代码,而Thread类对于执行这些代码的具体实现过程就是运行run方法,如果我们不去覆盖Thread类的run方法,那么他运行的是Thread类中的run方法,这样我们开启线程的就没有意义了,所以必须要复写run方法。
/*
创建线程的方法一:
1.创建一个类继承Thread类,
2.覆盖Thread的run()方法
3.创建Thread类的子类对象
4.调用start()方法开启线程
*/
//首先定义一个类继承自Thread类
class Mythread extends Thread
{
//复写Thread类中的run方法
public void run()
{
System.out.println("自定义线程开启");
}
}
class ThreadDemo1
{
public static void main(String[] args){
//利用多态的方式new出子类对象,也可以不使用多态
Thread t1 = new Mythread();
//调用strat()方法开启线程
t1.start();
//这句是主线程中的执行语句
System.out.println("主线程开启");
}
}
/*
* 打印:
* 主线程开启
自定义线程开启
**/
2.第二种方法:实现Runable接口
1>创建一个类实现Runnable接口
2>在类中复写run()方法编写我们要运行的代码
3>通过Thread类创建线程对象并将Runable子类对象作为Thread的构造参数进行传递
4>开启线程
/*
创建线程的方法二:
1.创建一个类实现Runnable接口
2.覆盖其中的的run()方法
3.通过Thread类创建对象并把我们定义的类的对象作为参数传入Thread构造函数中
4.调用start()方法开启线程
*/
//首先定义一个类实现Runnable接口
class Mythread implements Runnable
{
//复写Runnable中的run方法
public void run()
{
System.out.println("自定义线程启动");
}
}
class ThreadDemo
{
public static void main(String[] args){
//创建Thread对象,参数是我们自定义线程类的对象
Thread t2 = new Thread(new Mythread());
//调用strat()方法开启线程
t2.start();
//这句是主线程中的执行语句
System.out.println("主线程启动");
}
}
/*
* 打印:
* 主线程启动
自定义线程启动
**/
我们在开发时两种方式都可以使用,但是推荐使用第二种方式,这一种方式的好处在于
①Runnable接口中有且只有一个Run方法,我们可以只拿过这个方法来使用,而不用去继承Thread类中所有方法
②我们按照面向对象的特点将我们要执行的任务封装成对象,这样就从Thread的子类中分离出来了。
③ 我们在实现Runnable接口的同时还可以去继承其他的类,避免了单继承的局限性。
3.线程名称的获取与写入
1.获取线程的名字
1>getName()
我们可以通过Thread的方法getName()获取线程的名称,当我们创建了一个线程对象,这个线程的名字如果我们不指定的话就java会给我们默认的名称:Thread-加上编号。
2>currentThread()方法
这个方法也是Thread类中的方法,他返回的是当前运行中的线程对象引用,我们要拿当前线程的名称,必须要先获取对象,因为我们的线程是通过对象创建出来的,这个方法返回当前运行时的线程对象,我们再利用getName()方法就可以获得当前运行线程的名称了。
主线程的名字时固定的:main
2. 定义线程的名字
1>当我们使用第一种继承Thread类的方法时,因为Thread类中有构造方法Thread(String name),所以我们可以直接在子类中使用super调用即可。示例:
/*继承Thread方式时定义线程名称
*利用super关键字调用父类中的构造方法起名
* */
class Mythread extends Thread
{
Mythread(String name)
{
//调用父类中的构造方法为线程起名
super(name);
}
//复写Thread中的run方法
public void run()
{
//获取当前运行线程的名字
System.out.println(Thread.currentThread().getName()+"线程开启");
}
}
class ThreadDemo
{
public static void main(String[] args){
//创建线程1名字叫小宏
Thread t1 = new Mythread("小宏");
//创建线程2名字叫小宏
Thread t2 = new Mythread("小绿");
//调用strat()方法开启线程
t1.start();
t2.start();
}
}
/*
* 打印:
* 小宏线程开启
小绿线程开启
**/
2>实现Runable接口定义线程名字
实现Runnable接口时为线程赋名的方法有两个,第一种:通过Thread类的构造方法,在Thread类中有这样一个构造方法:Thread(Runnable targer,String name),把自定义类对象和线程名字作为参数传递给构造方法。第二种:Runable接口中也没有构造方法,除了个run方法穷的连条秋裤都没有,java中的上帝(Object)显灵了,他说我有啊,我有个setName(),来,跟我混吧,有肉吃。
/*实现Runnable方式时定义线程名称
* 第一种:通过Object类的setName
* 第二种:通过Thread类的构造函数
* */
class Mythread implements Runnable
{
//复写Runnable中的run方法
public void run()
{
//获取当前运行线程的名字
System.out.println(Thread.currentThread().getName()+"线程开启");
}
}
class MyThreadDemo
{
public static void main(String[] args){
//创建线程1
Thread t1 = new Thread(new Mythread());
//我们利用上帝setName方法为线程起名
t1.setName("小宏");
//创建线程2,利用Thread的构造方法为线程起名
Thread t2 = new Thread(new Mythread(),"小白");
//调用strat()方法开启线程
t1.start();
t2.start();
}
}
/*
* 打印:
* 小白线程开启
* 小宏线程开启
* */
4. 线程的使用示例
说了这么多我们来写个小程序来演示一下线程的基本使用,我们有一个卖票的需求,我有100张票,一个售票员卖太慢了,我们让四个售票员卖。
/*
* 需求:利用4个售票员卖100张票
* 为了提高效率我们开启四条线程同时卖这100张票
* 注意点:
* 100张票是固定的,如果我们利用继承Thread类的方法,就需要
* new四个对象来构建4条线程,这样我们的票就变成了400张,所以不可以用此方法
* */
//定义一个类继承Runnable接口
class Mythred implements Runnable
{
//定义100张票,因为票数是固定的,定义在成员位置上
private int num =100;
//覆盖run方法
public void run()
{
//while循环方法
while(true)
{
if(num>0)
System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"票");
else
return;
}
}
}
class ThreadTest{
public static void main(String[] args) {
//因为我们要确定票数是固定的,所以对象只能一个
Mythred mt = new Mythred();
//定义线程1,起名小宏
Thread t1 = new Thread(mt);
t1.setName("小宏");
//定义线程2,起名小绿
Thread t2 = new Thread(mt);
t2.setName("小绿");
//定义线程3,起名小蓝
Thread t3 = new Thread(mt);
t3.setName("小蓝");
//定义线程4,起名小白
Thread t4 = new Thread(mt);
t4.setName("小白");
//开启这四条线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
这个示例要打印的实在是太多了,而且可能每次打印的都不一样,我们就不在这里列出来了,打印情况可能是单独的一个售票员把所有的票的卖完了,因为cpu的控制权一直在这个售票员手里,也有可能是多个售票员共同卖出的,因为cpu在不同线程间进行着切换。在打印的时候卖票的顺序不是固定的,有可能是先卖了第4张,再卖了第3张,这是因为我们的控制台也是一个应用程序,cpu也在做着切换动作。
三. 多线程的安全问题
在卖票的示例中,我们只是简单演示了多线程的使用,如果我们多运行几次就会发现可能有卖出第0张票的情况发生,这是肿么回事,售票员傻了么,原因就是我们使用多线程操作共享资源时可能会发生数据的不一致。我们来看这句代码,使用if判断票数是否不为0:
if(num>0) System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"票");
我们来分析一下这句话,如果发生了这种情况,当num为1,线程1判断num之后还没有输出,线程2获取到cpu的执行权,他判断num为1然后输出,卖出了最后的这张票,num—就变为0,但是当线程1又获取到了cpu的执行权,他往下执行,就把0号票卖出去了。这就造成了我们程序的安全隐患,怎么来解决这种问题呢,java为我们提供了同步的思想。
1. 同步的思想
我们将多条线程要操作共性数据的线程代码封装起来,当一条线程在运行这些代码时,其他线程不可以参与进来,这种思想就是同步。
锁的概念:每一个线程要运行被同步后的内容,必须要拿到这个同步内容的锁,这也是保证同步内容不能被多条线程并发操作的原因,当一个线程拿到了锁他就可以运行同步内容,而其他没拿到锁的线程只能在同步外等待,这个锁存在于每一个对象上,也就是每一个对象都有一个锁。锁可以是对象,也可以是一个class文件。
同步关键字:synchronized
同步代码块:synchronized(对象){需要被同步的代码}
同步函数:将synchronized作为修饰符创建一个函数
例:public synchronized voidshow(){需要被同步的代码}
同步函数与同步代码块的区别:
同步代码块中我们可以自定义对象来作为锁,而同步函数中的锁则是固定的,为当前对象(this)。当我们定义的同步函数时静态的时,他就不能调用对象作为锁了,这时候他的锁为当前类的字节码文件(类名.class)。
2. 同步的弊端
使用同步时,程序的效率降低了,因为每一个线程都会去判断同步锁,当一个线程拿着锁的时候,如果cpu切到了其他线程,那么同步外的线程会不断判断锁,由此效率降低
3.同步的前提
我们使用同步是为了解决多线程的安全问题,所以当有多个线程操作公共资源时要使用同步,而且同步的锁必须是唯一的,如果多个线程持有多个锁,那么就会不止一个线程去操作同步中的内容,同步的意义就不存在了。
4.单例模式下的多线程问题
当我们使用懒汉式单例设计模式时,就会出现线程安全问题,我们知道单例设计模式是只可以new一个对象的,而懒汉设计模式是先判断有无对象,如果没有再去new对象,但是如果当一个线程a判断了该对象为空,但是cpu切换到了另一个线程b,这时候还是没有对象被创建的,线程b创建了一个对象,而当cpu切换到线程a时,a已经判断完成了,继续向下执行又会创建出一个对象,那么我们的单例设计模式就失效了。所以我们可以利用同步来解决该问题。
//懒汉式单例设计模式下的多线程安全问题
class Demo{
private static Demo d = null;
private Demo(){}
public static Demo getInstance()
{
//此处判断d是否为空是为了提高效率
if(d == null)
{
//加入同步代码块中后,就只能有一个线程来new对象了
synchronized(Demo.class) {
if(d==null)
d = new Demo();
}
}
return d;
}
}
5.死锁
死锁的常见情景之一:嵌套
就是一个run方法中有两个锁,这两个锁你中有我,我中有你,当线程A拿到锁1,想去拿锁2时,cpu切换到线程B,线程B先去拿锁2,然后去拿锁1,但是锁1在线程A手中,注意当一个线程没有执行完同步中的内容时他是不会自动抛锁的,所以当cpu切换到线程B时,线程A不会抛弃锁1,这时候两边都没有执行完同步的内容,无论cpu切换到谁,谁都无法去获取对方的锁,这就产生了死锁的现象。
死锁是我们在写程序时要避开的坑,这种问题一旦发生将会对我们的程序造成10000+的伤害。
死锁示例:
class Lock implements Runnable
{
//定义标志
boolean flag = true;
//自定义一个对象作为锁
private Object obj = new Object();
public void run()
{
//如果标记位真先拿this锁,再拿obj锁
if(flag)
{
//线程拿到this锁
synchronized (this) {
System.out.println("if....this");
//当线程拿到this后让他休息50ms,以便让另一线程去else中拿obj锁
try {
Thread.sleep(50);
} catch(InterruptedException e) {
e.printStackTrace();
}
//线程拿到this锁后又拿obj锁
synchronized (obj) {
System.out.println("if....obj");
}
}
}
//标记为假,先拿obj锁再拿this锁
else
{
//线程拿到obj锁
synchronized (obj) {
System.out.println("else....obj");
//线程拿到obj锁后又拿this锁
synchronized (this) {
System.out.println("else....this");
}
}
}
}
}
class ThreadDemo3 {
public static void main(String[] args) {
Lock t = new Lock();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//开启线程执行run方法中的if语句
t1.start();
//主线程睡10ms,标志位置为假
try {
Thread.sleep(10);
} catch(InterruptedException e) {
e.printStackTrace();
}
//标志位置为假
t.flag = false;
//开启线程执行run方法中的else语句
t2.start();
}
}
/*
* 打印:
* if....this
else....obj
这就是线程1拿到this锁,线程2拿到obj锁,然后去拿对方的锁,结果谁都不放手
程序就卡在这里了。
* */
四. 多线程的通信
当有多个线程操作一个公有资源时,我们知道要加同步来确定只有一个线程来操作共享数据,以免造成数据的混乱,这种情况是cpu是随机切换线程的,有可能一个线程会多次操作共享资源,当我们希望一个线程操作完成后就让另一线程来操作此资源,这就涉及到了多线程的通信问题。
1.线程间通信方法
1>wait()方法
此方法让当前线程抛弃同步锁,并让出cpu的执行权,进入冻结的状态,当其他线程唤醒他时他才会醒,如果不唤醒他,他就一直睡觉。
2>notify()
此方法是唤醒被wait()后的线程,他唤醒的是第一个被wait()的线程。
3>notifyAll()
此方法是唤醒所有的wait()后线程
注意点:这些方法都应用于同步中的,我们要让一个线程等待或者被唤醒是通过操作锁(监视器)来完成的,所以要应用在同步中,当一个线程拿到锁后,因为可能会有多个锁的情况,而我们到底想让哪个线程同步执行完后等待或者唤醒必须是依靠锁来确定的,
这三个方法是Object中的方法,并且是通过锁也就是对象来调用的,而锁可以是任意对象,所以要定义在Object中。
简单说就是等待和唤醒必须是同一把锁,不能是这个锁去唤醒另一个锁上的线程。
2. 线程间通信的举例
当我们有两类线程来操作一个公有资源,一类是往资源里面添加,一类是取出,我们希望添加一次取出一次,一般我们的做法是在公有资源中定义一个标志位,把这个公有资源放到同步中,当添加完成后,标志位改变,唤醒其他线程,使用wait方法,然后让此类线程等待,继而取出线程开始执行,当取出后,标志位改变,唤醒其他线程,再利用wait方法,让取出线程等待,如此可以实现加一次,取一次。
注意点是当取出和添加不止一个线程时,标志位的判断不能是if了,要用while,让此类线程循环判断该标记,唤醒时也不能用notify(),而是用notifyAll(),唤醒所有的线程,避免出现全部线程等待的情况。
示例:
//我们把公有资源封装到类中
class Commodity
{
//定义标志位
boolean flag = false;
int count = 0;
//定义添加方法,注意要加同步
public synchronized void set()
{
//标记为真,添加线程等待
while(flag)
{
try {
this.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...商品生产..."+(++count));
//计数器加1后,标志位置为真
flag = true;
//唤醒所有等待线程
this.notifyAll();
}
//定义取出方法
public synchronized void get()
{
//如果标志位为假,取出线程等待
while(!flag)
{
try {
this.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
//把计数器值显示出来
System.out.println(Thread.currentThread().getName()+"...商品消费......"+count);
//标志位置为假
flag = false;
//唤醒所有线程
this.notifyAll();
}
}
//定义添加线程
class Input implements Runnable
{
private Commodity s ;
Input( Commodity s) {
this.s = s;
}
public void run()
{
while(true)
{
s.set();
}
}
}
//定义取出线程
class Output implements Runnable
{
private Commodity s;
Output( Commodity s) {
this.s = s;
}
public void run()
{
while(true)
{
s.get();
}
}
}
class Test1 {
public static void main(String[] args) {
Commodity s = new Commodity();
Thread t1 = new Thread(new Input(s));
t1.setName("小宏");
Thread t2 = new Thread(new Input(s));
t2.setName("小绿");
Thread t3 = new Thread(new Output(s));
t3.setName("小蓝");
Thread t4 = new Thread(new Output(s));
t4.setName("小黑");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
* 打印:
* 小宏...商品生产1
小黑...商品消费...1
小绿...商品生产2
小黑...商品消费...2
小宏...商品生产3
小蓝...商品消费...3
.....
* */
3. java1.5版本后的锁
java1.5版本后出现了接口Lock和Condition。Lock替代了锁,Condition替代了锁上的方法,一个锁可以关联多个Conditio对象,这样我们就把锁和方法分离开来,这样做的好处在于一个锁对应了多个等待唤醒机制,以前我们使用notifyAll()来唤醒锁上的所有线程,而出现了这两个接口后我们就可以精确到底唤醒哪一类线程了。
(1)Lock接口中的方法
1> lock()
获取锁
2> unlock()
释放锁
也就是说以前的同步中拿锁、释放锁是程序自动完成的,而现在需要我们手动来拿锁释放锁,需要注意的是unlock()是必须要执行的方法,我们要把他放在finally里面来,无论他是否抛出异常,释放锁这步是必须要做的。
3>newCondition()
获取到与此锁相关联的Condition实例对象,我们说Condition替代了锁上的方法,无论是等待还是唤醒都是与锁关联在一起的,所以要通过锁来拿到这个对象。
(2)Condition接口的方法
1>await()
让线程进入等待状态,类同与同步中的wait()方法
2>signal()
唤醒一个等待的线程,类同与同步中的notify()方法
3>signalAll()
唤醒所有的等待线程 ,类同与同步中的notifyAll()方法
(3)分离锁上的等待唤醒机制,以Lock的子类ReentrantLock为例说明
Lock l = new ReentrantLock();
Condition lock_int = l.newCondition();
Condition lock_out = l.newCondition();
现在我们有一个锁l,而l对应了两个condition对象,也就是说我们可以指定让哪个Condition等待或者被唤醒,例如lock_int .await()对应lock_ou.singal,这样一类线程等待,而另一类线程被唤醒,不再是使用notifyAll()或者signalAll()来唤醒所有的线程,这样我们的程序的严谨性和准确性就提高了。
五. 线程的状态
1.线程的状态也称之为线程的生命周期,我把线程从出生到结束归类为6种状态。
(1)初始状态,这时候线程刚被初始化完,也就是刚new出来一个Thread对象。
(2)可运行状态:调用Start()方法,线程才真正被创建出来,但是并没有拿到cpu执行权。
(3)运行状态:被cpu切换到的线程,他具有cpu的执行权
(4)睡眠状态:当调用sleep()方法时,导致线程进入睡眠状态,但是线程不会抛弃锁。
(5)等待状态:当调用wait()方法时,线程进入等待状态,并把手中的锁抛弃,
我们把睡眠状态和等待状态又合称为冻结状态。
(6)终止状态:指线程运行结束。
2. 停止线程的方式:java中以前给我们提供了stop()方法来停止线程,但是很不幸的是此方法有一些bug,所以java不再让我们用此方法停止线程。我们用来停止线程的方式是让run方法结束。
我们开启多线程时运行的代码通常都是循环结构的,只要控制住循环,就可以让run方法结束。但是当我们的线程在冻结状态时,即使我们控制住循环,即循环的条件不满足时我们的程序仍然停不下来,这时我们就要用到interrupt()方法,这个方法可以将等待中的线程强行中断,重新回到运行状态中来,这样我们就可以结束线程了,但是这种方法是强制唤醒线程,会抛出一个InterruptedException异常。
让我们看一看线程中各个状态的流程图:
六. 线程的其他方法
1. setDeamon(boolean flag)
在线程开启前让其成为守护线程,也就是必须在start()方法前使用。
守护线程类似于后台线程,当前台线程运行时,守护线程也运行,并无多大区别,但是如果前台线程退出了,只剩下后台线程时,无论后台线程有没有执行完相关操作,都会直接挂掉。也就是当程序只剩下守护线程的时候程序结束。
2. join()
当一个线程使用了join()方法,其他线程会抛弃cpu的执行权,当此线程结束是,其他线程才会运行,join()让线程获取到执行权直至结束。比如线程A调用到了join()方法,那么其他线程就挂起,直到线程A执行完线程B才能运行。
3. yield()
暂停当前正在运行的线程,然后去执行其他线程。