java 多线程
1.什么是进程?
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,
包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储
处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动
过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有
处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。就是每个正在系统上运行的
程序都是一个进程。
*********************************************************************************************************
* 进程是一个包含自身执行地址的程序,在一个多任务的操作系统中(像window),可以分配cpu时间给每
* 一个进程,cpu在某个时间段执行某个进程,然后先一个时间片执行另一个进程,由于转换速度很快使
* 得每个程序好想在同时运行。
*
* 线程是进中的执行流程,一个进程中可以包含多个线程,也就是说一个程序中同时可能进行多个不同
* 的子流程,每个子流程可以得到一小段程序的执行时间,每执行完一个线程就会跳到下个线程,由于
* 转换的速度很快,这使得一个程序好像能同时执行多个事务。
*
* 就是说多线程的时候,cpu执行到谁谁就运行,但是一次只能执行一个(多核除外)。我们可以形象的把
* 多线程的运行形容为在互相抢夺cpu的执行权。这就是多线程的一个特性:随机性。谁抢到谁执行,至
* 于执行多长,cpu说的算。
*********************************************************************************************************
2.什么是多线程?
线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主
执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线
程终止了,进程也就随之终止。
*******************************************************************************************************
* 每一个进程包含一个或多个线程,最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车
* 厢离开火车是无法跑动的,同理火车也不可能只有一节车厢。
*
* 多线程举例:
* 每一个进程就像是一条直线,单线程的话就是愣着头什么都不管从头走到尾,你有别的什么事得先等
* 我把当前任务完成了再去看。就像java虚拟机,主线程就是main函数,另外虚拟机里面还有个垃圾回
* 收机制,在执行程序的过程中,同时也能去处理垃圾,这就是多线程。
*
* 比如说唐僧让悟空和猪八戒去打扫房间了,说,你们给我干活去,然后他哥俩就闷着头去干活了。当
* 然了打扫房间肯定有很多活要干啊,猪八戒就开始闷着头干,先扫完地再去倒垃圾然后再去擦玻璃什
* 么的,因为他就一个人(单线程)啊,所以只能干完这个了在去干那个。悟空就不一样,拔出几根毫毛
* 变成好几个人,然后就和那几个人说:"我擦玻璃,我擦的过程中,你负责扫地,你负责倒垃圾...."
* 然后就各忙各的,这样是不是效率就快了很多啊。
*
* 另外迅雷下载也是多线程,就一个线程从头下到尾是不是很慢,多个线程的话,就这个线程下载这部
* 分,另个线程下载那部分,这样速度是不是就很快了。
******************************************************************************************************
3.线程的5种状态
01.新生态(new Thread)
一个线程刚被new运算符生成的状态叫新生态,也就是刚被创建的状态,例如下面的代码就处于
新生态。 Thread t = new Thread();
当线程处于新生态时,即已经被创建但没有执行start()方法时,它仅仅是一个空线程对象,系
统不会分配资源给它。
02.可运行状态(Runnable)
线程执行start()方法之后便进入可运行状态,但是不一定被立即执行,但是CPU随时可能被分给
该线程,从而执行该线程。例如:
Thread t = new Thread();
t.start();//进入可运行状态
03.运行状态(Running)
正在运行的线程处于运行状态,此时该线程独占CPU的控制权,若有更高优先级的线程出现,则该
线程放弃CPU执行权进入可运行状态也可以使用yield()方法使处于运行状态的线程主动放弃CPU执
行权,也有可能是执行结束或执行到stop()方法放弃控制权,进入死亡状态。
04.阻塞状态(Block)
由于某种原因,是的运行中的线程不能运行,该线程处于阻塞状态,此时县城不会被分配到CPU的
控制权,无法执行,能阻塞线程的方法主要有:
sleep()方法:睡眠方法,在指定的时间内让线程处于阻塞状态,指定的时间一过此线程又处于
可执行状态,就是你告诉它睡多长时间,时间一到它就醒了然后进入可运行状态
wait()方法:等待方法,它有两种格式:一种是你告诉它等多长时间,在等的过程中用notify()
方法唤醒它,或者是等的时间一到它自己醒来进入可运行状态。另一种格式必须
是用noytify()方法来唤醒它进入可运行状态。
05.死亡状态(Dead)
一般情况下当线程结束后进入死亡状态,有两种情况可导致线程进入死亡状态,即自然撤销或被
停止,当运行run()方法结束时,该线程就自动撤销,当一个应用程序因故停止运行时,系统将
停止该程序正在执行的所有线程。当然也可以调用stop()方法来终止线程。
4.线程的创建方式
创建线程有两种方式:一、继承Thread类 二、实现 Runnable接口
1.继承Thread类来创建线程
通过查看java的API发现,java把线程这类事物也封装成了对象。就是Thread类。
继承Thread类创建线程的步骤为:
01.继承Thread类
02.复写Thread中的run()方法,把需要让这个线程运行的代码放在run()方法中。
03.创建Thread类的子类对象,也就是创建线程对象;
04.调用线程的start方法,来开启线程(并调用run()方法)。
部分代码:
class PrimeThread extends Thread
{
public void run()
{
//此线程要运行的代码
}
}
然后,下列代码会创建并启动一个线程:
class ThreadDemo
{
public static void main(String[] args)
{
PrimeThread p = new PrimeThread();//创建线程
p.start();//调用start方法来启动线程 注意是start()
}
}
************************************************************************************
为什么是p.start()而不是p.run()?
因为调用run方法仅仅是在单纯的调用run()方法,而没有开启线程。而start()的意思是
开启线程并且调用run()方法来运行run()方法里的代码。
为什么要覆盖run() 方法呢?
Thread类用于描述线程。该类就定义了一个功能,用于存储线程中要运行的代码。该存储
功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
**************************************************************************************************
另外每个线程都有他自己的名称:Thread-编号 该编号从0开始。
既然县城也被java封装起来了,那么他肯定也有自己的方法,和自己的构造函数,通过查看API可
以发现 Thread类中有能获得线程名称的方法,就是getName(): 用来获取线程名称。当然也可以通
过构造函数或setName()来设置线程名称。static Thread currentThread():获取当前线程对象。
部分代码:
class Test extends Thread
{
Test(String name)
{
super(name);//Thread类中有设置名称的方法和构造方法,子类直接拿来用就可以了
}
public void run()
{
for(int x=0; x<60; x++)
{
//currentThread()获取当前线程对象
System.out.println((Thread.currentThread().getName()+" run..."+x);
}
}
}
class ThreadTest
{
public static void main(String[] args)
{
Test t1 = new Test("one---");
Test t2 = new Test("two+++");
t1.start();
t2.start();
for(int x=0; x<60; x++)
{
System.out.println("main....."+x);
}
}
2.实现 Runnable接口来创建线程
**************************************************************************************************
步骤为:
01.定义类实现Runnable接口
02.覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
03.通过Thread类创建线程对象
04.把Runnable子类对象当参数传给Thread类的构造函数
05.调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。你创建了线程之后,要
让那些线程运行哪个对象里的run方法啊,要给它指定一下。所以要让线程去运行指定
对象的run方法。就必须明确该run方法所属对象。
**************************************************************************************************
购票程序部分代码:
class Ticket implements Runnable//实现Runnable接口
{
public void run()//复写Runnable里的run方法
{
//需要执行的代码
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();//创建Runnable子类对象
//创建了一个线程;并把所执行run()方法所属的对象当参数传给构造函数
Thread t1 = new Thread(t);//要给创建的线程指定一下运行那个对象的run方法
Thread t2 = new Thread(t);//否则的话,它就执行它自己的run方法,而他自己的
Thread t3 = new Thread(t);//run方法里什么都没有。
t1.start();//开启线程
t2.start();
t3.start();
}
}
**************************************************************************************************
* 继承方式创建线程与实现方法创建线程有什么区别?*******
* 如果用继承方法创建的话,那这个类就不能就成别的类了,别如说Student类继承了Person类,
* 那Student类里有需要多线程来执行的代码,那Student类就不能继承Thread类了,因为java中
* 不支持多继承,但是这时Student类可以通过实现Runnable类来达到这个目的。就是Student类
* 既能继承Person类,又能通过实现Runnable来达到多线程的目的。
*
* 实现Runnable方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。
*
* 两种方式区别:线程代码存放的位置不同
* 继承Thread:线程代码存放Thread子类run方法中。
* 实现Runnable,线程代码存在接口的子类的run方法中而不是在现线程(Thread)的run方法中。
*****************************************************************************************************
5.多线程的安全问题
购票程序代码:
class Ticket implements Runnable
{
private int tick = 100;//四个线程共享100张票
public void run()
{
while(true)
{
//为什么会出现0,-1,-2的错票呢?
if(tick>0)
{
//因为是四个线程,当1线程进来后,有可能会停在这,没有执行权了,然后2程也线
//进来了也停在这了,这时候1线程活了卖出了最后一张票,这时候没票了,而2线程已
//经不需要判断了,因为它是判断之后进来的,这时候它就打印的tick--就是0号票了
try{Thread.sleep(10);}catch (Exception e){System.out.println("出异常了");}
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
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();
}
}
通过分析,发现此程序有可能打印出0,-1,-2等错票。说明多线程出现了安全问题,为什么呢?
参看下图:
**************************************************************************************************
* 多线程出现安全问题的原因是:
* 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执
* 行完,另一个线程参与进来执行。导致共享数据的错误。就是多个线程同时访问一个数据是
* 会产生冲突。
*
* 解决多线程安全问题的办法就是:
* 当多个线程操作它们共享的数据时,一个线程都执行完,才允许下个线程进来,一个线程没
* 执行完另一个线程不准执行,
*
* 举个例子:
* 比方说一个房间里有好多苹果,然后分给大家,条件是一次只能拿一个,房间外面有个
* 牌子会显示房间里苹果的剩余个数,如果为0的话就不用进去了,这时候外面牌子上显
* 示的是还剩余一个,这时候甲来了,发现还有一个就推门进去了,但是就去之后甲睡着
* 了,这时候乙线程看到牌子上显示的是还剩余一个,他也推门进去了,然后他也睡着了,
* 这时候甲醒了,把那一个拿走了,但是等乙醒了之后再去拿是不是就没啦,这孩子就该
* 纳闷儿了,现实的里面有1个我才进来的怎么没了呢,就那个零蛋走了。解决办法就是:
* 当甲推门进来之后,乙必须等甲执行完拿苹果的动作之后才能进来,否则就算甲死里面
* 你也不许进。这样甲进来之后无论睡多长时间醒来之后拿着就走了,这时候外面显示剩
* 余个数为零,乙就不用进来了。这样就不会出现问题了,
**************************************************************************************************
6.同步代码块
Java对于多线程的安全问题提供了专业的解决方式,就是同步代码块。
格式为:
synchronized(对象)
{
需要被同步的代码
}
对象就如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也
进不去,因为没有获取锁。
**************************************************************************************************
* 同步的前提:
* 1,必须要有两个或者两个以上的线程。
* 2,必须是多个线程使用同一个锁。
* 未满足这两个条件,不能称其为同步。
* 必须保证同步中只能有一个线程在运行。
*
* 例子:火车上的卫生间---经典。你进去以后,把门在里面一反锁,门上的标识就由绿变红了,
* 外面的人再想进的话就得判断一下里面有没有人,里面有人的话,他是开不了门进不去的,
* 另外,他如果去的是另一个车厢的卫生间,你再怎么锁和他有关系吗?
*
* 同步的好处:解决了多线程的安全问题。
* 同步的弊端:
* 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降
* 低程序的运行效率。
**************************************************************************************************
7.同步函数
查看多线程时会不会出现问题,要注意以下几个明确:
1,明确哪些代码是多线程运行代码。
2,明确共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
格式:
在函数上加上synchronized修饰符即可。
同步函数用的是哪个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的
锁是this。
**************************************************************************************************
通过售票程序进行验证。使用两个线程来卖票。一个线程在同步代码块中。一个线程在同步函
数中。两个线程都在执行卖票动作。如果同步代码块的锁是this的时候没有出现问题就说明同
步函数的锁是也this。因为这样才满足那两个条件:1.是否多个线程2.是否是同一个锁,只有
是同一个锁的时候才不会出问题。
部分代码:
class Ticket implements Runnable
{
private int tick = 100;
Object obj = new Object();
boolean flag = true;//定义一个标记
public void run()//在主函数中定义两个线程,都执行此run方法里的代码
{
if(flag)//如果满足条件让其中一个线程走这条路(同步代码块这条路)
{
while(true)
{
/**如果此同步代码块的锁是obj,打印结果仍有错票,说明不满足上面所说的几个明确
*首先可以看出此程序是多个线程,但是怎么还有错票呢,那么就是不满足另一个,
*多个线程用的不是同一个锁*/
synchronized(this)//把同步代码块的锁改为this之后,问题解决了说明什么?
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
else
while(true)
show();
}
public synchronized void show()//只把操作共享数据的代码写进同步函数中
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
}
}
}
8.静态同步函数的锁
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没
有本类对象,但是一定有该类对应的字节码文件对象。类名.class 该对象的类型是Class
所以静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
单例设计模式
//懒汉式
class Single
{
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。
**************************************************************************************************
9.死锁
什么是死锁?
所谓死锁就是指两个以上的线程互相都有要求使用对方已经占有的资源而导致无法继续运
行的现象。就是同步中嵌套同步。
简单的一个死锁程序代码:
public class DeadLock
{
private static final Object obj1 = new Object();
private static final Object obj2 = new Object();
public class Run1 implements Runnable
{
String threadName = Thread.currentThread().getName();
public void run()
{
synchronized(obj1)
{
System.out.println(threadName+"进入同步块obj准备进入obj2");
synchronized(obj2)
{
System.out.println(threadName+"已经进入同步块obj2");
}
}
}
}
public class Run2 implements Runnable
{
String threadName = Thread.currentThread().getName();
public void run()
{
synchronized(obj2)
{
System.out.println(threadName+"进入同步块obj准备进入obj1");
synchronized(obj1)
{
System.out.println(threadName+"已经进入同步块obj1");
}
}
}
}
public static void main(String[] args)
{
Thread t1 = new Thread(new DeadLock().new Run1());
Thread t2 = new Thread(new DeadLock().new Run2());
t1.start();
t2.start();
}
}
******************************************************************************************
10.线程间通信
其实就是多个线程在操作同一个资源,但是操作的动作不同。
为设么会出现安全问题呢?首先看满足那几个明确吗?一,是多个线程吗?二,用的是同一个锁码?
三,再明确那些代码是它们操作共享的数据的语句,再然后怎么解决设置一个打印一个的问题呢?我们
可以这样,让线程1循环设置的时候,线程2不准动(wait);然后当线程1设置完一个之后,让它自己把CPU
的执行权交出去,同时再把线程2叫醒,然后线程1就自己进入wait状态等待线程2打印。此时线程2被叫
醒(notfy),具有了CPU的执行权。然后开始打印,打印完毕之后再把线程1叫醒,自己在进入wait状态等
待线程1设置。 这就是现线程等待唤醒机制。
11.等待唤醒机
就像上面所说的,线程1设置完了叫醒线程2去打印,线程2打印完了叫醒线程1去设置,这谁叫醒谁
是不是就相当于个监视器啊,线程1监视着线程2,我一设置完就把你叫醒,叫醒谁啊?是不是得叫
醒和你玩的那个啊?就是上面那个“明确”拿着同一个锁的线程,所示同步里才有的东西,所以wait
和notify()都用在同步里面。因为同步里面可以嵌套同步,所以那就得标示出wait和motify是哪个
同步里的,标示的方法就是。所属同步的锁.noty()或锁.wait()。
*********************************************************************************************************
* wait(),notify(),notifyAll(),这三个方法的特点?
* 1,这些方法存在与同步中。
* 2,使用这些方法时必须要标识所属的同步的锁。
** 3,锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。
*
* 思考1:wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?
* 从java API中发现wait(),notify(),notifyAll()都定义在Object中,因为调用那三个方法的锁
* 可以是任意对象,而可以被任意对象调用的方法不就得在Object类中吗?
****************************************************************************************************************
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。
Lock:替代了Synchronized
lock
unlock
newCondition()
Condition:替代了Object wait notify notifyAll
await();
signal();
signalAll();
12.停止线程
stop方法已经过时。
如何停止线程?
只有一种,run方法结束。
开启多线程运行,运行代码通常是循环结构。
只要控制住循环,就可以让run方法结束,也就是线程结束。
特殊情况:
当线程处于了冻结状态。
就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态是,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。
Thread类提供该方法 interrupt();
13.守护线程(setDaemon)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何
参数。这可能抛出 SecurityException(在当前线程中)。
举个例子,孙悟空哥仨是不是要保护唐僧取取经啊?为什么要保护啊?因为唐僧一挂他哥仨
是不是就失业啦,存在也就没什么意义啦。这个唐僧就相当于前台线程,悟空它们就是守护
线程,前台线程一挂守护线程存在也就没意义了,这个程序也就结束了。JVM就退出了。
14.Join方法
当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。
join可以用来临时加入线程执行。让这个线程先运算完在运行其他的。
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.join();//主线程运行到这时,t1申请加入线程,就是抢夺CPU执行权,这时主线程就把执行权给t1了
t2.start();//等t1忙完了,执行权才到主线程手中,然后接着往下执行
15.多线程优先级&yield方法
toString
public String toString()返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
覆盖了 Object 类中的 toString()返回:该线程的字符串表示形式。
yield
public static void yield()暂停当前正在执行的线程对象,并执行其他线程。
**************************************************************************************************************
本章需注意的知识点:
01.举几个多线程在现实生活中的实例?
02.如何实现启动线程?
03.创建多线程的两种方法?步骤是什么?
04.继承创建线程与实现创建线程的区别?
05.为什么要将Runnable接口的子类对象传递给Thread的构造函数?
06.多线程出现安全问题的原因以及解决办法?(同时操作了共享数据,同步)
07.怎么看多线程是否会出现问题?(三个明确)
08.同步代码块,同步函数,静态同步函数的锁分别是什么?
09.死锁是怎么产生的?会写一个死锁程序?
10.等待唤醒机制中那三个方法的特点?
11.wait(),notify(),notifyAll()为什么定义在了Object类中?
12.如何创建一个后台线程?
13.何创建一个后台线程?(守护线程)
14.如何是一个线程休眠?
15.wait()与sleep()的区别?
16.线程的属性有什么作用?
17.JDK1.5 中提供了多线程升级解决方案是什么?
18.单例设计模式(懒汉式)所涉及的知识点?