java_day10_多线程

第十章:线程

1.进程和线程的概述
1)进程和线程定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

2)进程和线程关系
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,并且线程拥有自己的栈空间.

一个程序至少有一个进程,一个进程至少有一个线程,同时线程不能脱离进程而单独存在

3)进程和线程区别
进程和线程的主要区别在于它们是操作系统不同的资源管理方式。进程有独立的地址空间,一个进程崩溃后,一般是不会对其它进程产生影响;而线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程没有单独的地址空间.

4)操作系统中的进程和线程
在操作系统中,以多进程形式,允许多个任务同时运行(其实是进程之间切换运行的);以多线程形式,允许单个任务分成不同的部分运行(每个部分的代码由一个线程来负责执行)。

注:可以看出来一个应用程序的代码,主要是由线程负责在内存中执行,同时这些代码可以分为不同的部分交给多个线程分别执行,在线程执行代码过程中,如果需要用到计算的机资源,那么就可以从线程所属的进程中获取,而进程则是操作系统进行资源分配和调度的独立单位。


思考:为什么运行我们运行的java程序的时候要先启动JVM虚拟机?


2.java中的线程
1)Thread类
java.lang.Thread类
public class Thread extends Object implements Runnable{..}
Thread是java中的线程类,是对java中线程的抽象,Thread类型的对象就可以表示java中的一个线程

注:一个线程对象的作用就是可以单独运行我们所交给它的任务(代码)
注:Thread类及其子类的对象都可以表示一个线程对象

2)线程的分类
在Java中有两类线程:
用户线程 (User Thread)
也可以称为前台线程、执行线程
守护线程 (Daemon Thread)。
也可以称为后台线程、精灵线程(Daemon有精灵的意思)

守护线程,是指程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的退出: 如果用户线程已经全部处于死亡状态,虚拟机也就退出了,这是也不用管守护线程是否还存在了

注:java中创建出来的线程默认是用户线程,但是在启动线程前可以通过特定方法(setDaemon)把该线程转为守护线程


3)名字叫"main"的线程
当我们运行一个java程序的时候,其实就是让JVM创建一个名字叫"main"的线程,然后让这个线程去执行我们所编写的类中main方法的代码。我们可以把这个线程称之为main线程或主线程,因为这个线程是第一个执行我们编写代码的线程,但是这时候并不是只有这一个线程在JVM中,可以通过jconsole观察到当前的所有线程
注:jconsole是JDK自带的监测java程序运行的工具


4)多线程程序
我们之前所编写的代码绝大多数都是main线程执行的(单线程),但也有一些是多线程的程序。

由于java中允许在一个线程中创建并启动另一个线程,所以我们可以很容易的编程出一个多线程程序来。

思考:为什么要编写多线程程序,单线程程序不好么?


5)多线程程序的执行
为了提高程序执行效率,很多应用中都会采用多线程模式,这样可以将任务分解以及并行执行,从而提高程序的运行效率。但这都是代码级别的表现,而硬件上需要使用CPU的时间片模式来提供支持。程序的任何指令的执行都要竞争CPU这个最宝贵的资源,不论程序分成了多少个线程去执行各自的任务,这些线程都必须通过一定的方式来获取时间片,从而得到CPU的使用权进行代码的执行。

注:时间片就是CPU分配程序的使用时间,每个线程获得一个时间片后,在此段时间内是可以使用CPU进行运算的,但时间用完后就要交出CPU的使用权.

注:不同操作系统中,或者同类操作系统的不同算法中,时间片的大小是不一样的,但是不论哪种情况,对象我们来讲,这个时间片都是一个极短的一段时间.


让线程获得时间片的算法有多种,但是现在一般都是"抢占式",就是默认情况下,多个线程具有同等几率抢占到CPU的下一个时间片,最终谁能抢到那么这个时间片就算是谁的,使用完之后再退出来重新再争夺一下CPU的时间片

 


3.Thread类和Runnable接口
1)Thread类中的run方法
线程对象中的run方法,就是线程独立运行之后,必须要执行的方法,如果我们有什么代码要交给一个线程独立运行,那么就需要把这些代码放到run中(继承Thread重写run方法).

2)Thread类中的start方法
在代码中,我们并不能直接调用一个线程对象的run方法,而且需要调用线程对象的start方法来启动这个线程,然后这个线程会自动的调用run方法的,如果直接调用了run方法,那就不是多线程代码了

3)Thread类和Runnable接口的关系
Runnable接口中只有一个方法:
public interface Runnable{
public void run();
}

Thread类是Runnable接口的实现类,大致代码如下:
public class Thread implements Runnable{
private Runnable target;
public Thread(){}
public Thread(Runnable target) {
this.target = target;
}
public void run(){
if (target != null) {
target.run();
}
}
}

4)创建和启动线程
第一种方式:创建Thread的子类对象,子类中重写run方法
例如:
//如果需要,可以考虑使用匿名内部类
Thread t = new Thread(){
public void run(){
//代码...
}
};
//启动线程
t.start();


第二种方式:创建Thread类对象,在构造器中传Runnable接口的实现类,实现类中重写run方法
例如:
//如果需要,可以考虑使用匿名内部类
Thread t = new Thread(new Runnable(){
public void run(){
//代码...
}
});
//启动线程
t.start();

注:观察直接调用线程对象的run方法和start方法后有什么不同?

 

4.线程对象的状态
在java中使用枚举类型Thread.State可以表示出一个线程对象当前的状态,调用线程对象的getState()方法可以获得线程的当前状态
java.lang.Thread.State枚举类型
public class Thread implements Runnable{
public enum State {
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}
}

1)Thread.State枚举类型每个对象表示的状态含义
NEW(新建尚未运行/启动)
A thread that has not yet started is in this state.

一般是还没调用start方法,或者刚刚调用了start方法,start方法不一定"立即"改变线程状态,中间可能需要一些步骤才完成一个线程的启动。

RUNNABLE(可运行状态: 包括正在运行或准备运行)
A thread executing in the Java virtual machine is in this state.

start方法调用结束,线程由NEW变成RUNNABLE.
线程存活着,并尝试抢占CPU资源,或者已经抢占到CPU资源正在运行的状态都显示为RUNNABLE


BLOCKED(等待获取锁时进入的状态)
A thread that is blocked waiting for a monitor lock is in this state.

线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放。这时候线程B就是处理BLOCKED

WAITING(通过wait方法进入"无限期"的等待)
A thread that is waiting indefinitely for another thread to perform a particular action is in this state.

线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放(线程B处于BLOCKED状态),如果这时候线程A调用了wait方法,那么线程A就会马上交出CPU的使用权以及刚才拿到的锁,从而进入到WAITING状态,而线程B发现锁已经被释放了,线程B就从BLOCKED状态进入到了RUNNABLE,如果线程B拿到了锁之后在运行期间,调用了notify或者notifyAll方法,这时候线程A就会从WAITING状态进入到BLOCKED状态,从而等待锁的是释放.

TIMED_WAITING(通过sleep或wait等方法进入的"有限期"等待的状态)
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.

线程对象的sleep或wait等方法都可以传一个时间参数,表示就算没有其他线程调用特定方法来改变自己状态的时候,也可以通过这个时间参数让自己自动改变状态(因为时间到了)。

TERMINATED(线程终止状态)
A thread that has exited is in this state.

线程结束了,就处于这种状态,也就是run方法运行结束了。


2)通常对线程对象状态的描述
为了便于理解和记忆,通过会对Thread.State中定义的状态进行整理归类,最终可得到书中所描述的线程状态图。
图中将线程状态分为:
初始化状态
就绪状态
运行状态
死亡状态
阻塞状态(阻塞在等待池、阻塞在锁池、其他情况阻塞)

这样可以让我们对线程状态有一个更好的理解和掌握。


注:先整体了解下线程有哪些状态,等全面学完后再来验证每个状态的情况。

 

5.ThreadGroup线程组
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,对线程组的控管理,即同时控制线程组里面的这一批线程

用户创建的所有线程都属于指定线程组,如果没有显示指定属于哪个线程组,那么该线程就属于默认线程组(即名字叫"main"的线程组)
默认情况下,子线程和父线程处于同一个线程组

只有在创建线程时才能指定其所在的线程组,线程运行中途不能改变它所属的线程组,也就是说线程一旦指定所在的线程组,就直到该线程结束


1)创建线程组
java.lang.ThreadGroup类

创建线程组的时候需要指定一个线程组的名字,或者创建线程组的时候指定名字和它的父线程组。
创建线程组的时候需要指定线程组名字和它的父线程组,如果不指定其父线程组,那么默认是父线程组是当前线程组。(类中提供俩种构造器)
public ThreadGroup(String name);
public ThreadGroup(ThreadGroup parent, String name);

例如:
//获得当前线程的所属的线程组
ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();

//默认其父线程组是currentGroup
ThreadGroup tg1 = new ThreadGroup("线程组1");

//指定其父线程组tg1
ThreadGroup tg2 = new ThreadGroup(tg1,"线程组1");

2)线程和线程组
例如:
//不指定则属于默认线程组
Thread t1 = new Thread("t1线程");

//也可以指定线程组
ThreadGroup tg = new ThreadGroup("我的线程组");
Thread t1 = new Thread(tg,"t1线程");


6.Thread类中常用方法
注:Thread类中有一些方法已经被标注为过时,不推荐使用.
官网中还给出了弃用哪些方法的原因:
http://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html

第一类:静态方法
public static int activeCount()
返回当前线程的线程组中活动线程的数目

public static Thread currentThread()
返回对当前正在执行的线程对象的引用

public static int enumerate(Thread[] tarray)
将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中

public static void sleep(long millis)
让当前线程在指定的毫秒数内休眠

public static void yield()
暂停当前运行的线程,让给其他线程使用CPU执行

public static boolean holdsLock(Object obj)
判断当前线程先是否拿着指定的锁


第二类:非静态方法
public void run()
线程要执行的代码在此方法中
注:我们并不能直接调用run方法,而且启动线程后让线程自动调用run方法

public void start()
线程启动时必须调用的方法

public long getId()
返回该线程的标识符,线程ID是一个正的long数,在创建该线程时生成。线程ID是唯一的,线程终止时,该线程ID可以被重新使用

public void setName(String name)
设置该线程的名称
public String getName()
返回该线程的名称

public int setPriority()
设置线程的优先级
public int getPriority()
返回线程的优先级

public Thread.State getState()
返回该线程的状态

public ThreadGroup getThreadGroup()
返回该线程所属的线程组

public boolean isAlive()
测试线程是否处于活动状态

public void setDaemon(boolean on)
将该线程标记为守护线程或用户线程,默认false表示用户线程
public boolean isDaemon()
测试该线程是否为守护线程

public void join()
当前线程等待某个线程执行结束

public void join(long millis)
给定一个等待的限定时间

 

第三类:容易混淆的方法
public void interrupt()
中断线程
public boolean isInterrupted()
测试线程是否已经中断
public static boolean interrupted()
测试当前线程是否已经中断

如果线程a对象是处于阻塞状态的话,在线程b中调用a.interrupt()是会打断线程a的阻塞状态的(后抛出打断异常)
但是如果线程a对象是处于就绪等状态,在线程b中调用a.interrupt()只是会改变对象a内部的一个boolean类型标识,用来表示线程b想打断线程a

isInterrupted和interrupted的返回值就是这个boolean类型的值
区别在于静态方法interrupted在返回boolean值后,会把这个打断的标示符给清理掉,而且非静态方法isInterrupted不会清理


//观察Thread类中的部分源码
public class Thread implements Runnable{
public void interrupt() {
if (this != Thread.currentThread())checkAccess();

synchronized (blockerLock) {
Interruptible b = blocker;
//判断当前线程是否是阻塞状态
if (b != null) {
// Just to set the interrupt flag
interrupt0();
b.interrupt(this);
return;
}
}
//如果不是阻塞状态就只set一下打断的flag
interrupt0();
}
private native void interrupt0();


public boolean isInterrupted() {
return this.isInterrupted(false);
}

public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
}

 

7.数据共享
俩个或多个线程可以共享对象中的数据
例如:
public class ThreadTest extends Thread{
private Student student;
public ThreadTest3(String name) {
super(name);
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}

public void run() {
String name = Thread.currentThread().getName();
for(int i=0;i<10;i++){
//先把当前线程的名字设置为student的名字
student.setName(name);
//再马上拿出名字打印出来
System.out.println(name+": "+student.getName());
}
}
public static void main(String[] args) {
Student s = new Student();
ThreadTest t1 = new ThreadTest("线程1");
t1.setStudent(s);
ThreadTest t2 = new ThreadTest("线程2");
t2.setStudent(s);

t1.start();
t2.start();
}
}


例如:
Runnable run = new Runnable() {
private int x;
public void run() {
String name = Thread.currentThread().getName();
for(int i=1;i<=500;i++){
x++;
}
System.out.println(name+" x = "+x);
}
};
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t1.start();
t2.start();

 

思考:对象中的那些变量会被多个线程所共享,共享数据在多线程环境中会有什么问题?

 

8.非线程安全和线程安全
在上面的例子中可以看到,在多个线程并发访问的环境中对共享数据进行操作的代码或方法中会出现其结果不确定的情况,那么这段代码或者方法就是非线程安全的。产生这种情况的原因大多是因为一个线程在执行这段代码或方法的时间内,另一个线程也是有可能执行这段代码或方法的(抢夺时间片),从而造成了这段代码或者方法中对共享数据操作的结果

如果一段代码在多个线程并发访问的时候是 非线程安全的,那么就可以采用把这段代码进行线程同步的方式进行处理,最终把这段代码变为线程安全的

线程同步其实就是本来多个线程并发访问这段代码的,同步后就变成了一个线程一个线程的按顺序线程访问这段代码,这样以来一个线程在执行这段代码期间,就不用担心其他线程会来打扰自己在这段代码中对共享数据的操作了。

注:线程同步是牺牲了效率换来了安全


9.线程同步的实现
在java中,使用synchronized关键字来实现线程同步的效果。
synchronized关键字可以用来修饰方法,也可以直接作用到某段代码上

例如:
public class Test{
private int x;
public synchronized void test(){
String name = Thread.currentThread().getName();
for(int i=0;i<100;i++){
x++;
}
System.out.println(name+": x="+x);
}
}


例如:
public class Test{
private int x;
public void test(){
String name = Thread.currentThread().getName();
synchronized(this){
for(int i=0;i<100;i++){
x++;
}
}
System.out.println(name+": x="+x);
}
}



synchronized关键字是加锁的意思,用它来修饰方法就表示给该方法加了锁,从而达到线程同步的效果;用它来修饰代码块就表示给该代码块加了锁,从而达到线程同步的效果。

例如:一个方法test使用synchronized关键字修饰后加了锁,如果这个时候有俩个线程对象t1和t2要并发访问test方法,假设t1先抢到了CPU的执行权,从而率先拿到了test方法上的锁,然后就进到test方法中执行代码,一个时间片用完之后,就退回到就绪状态,但是t1线程却依然拿着锁,那么下次CPU的抢占即使是t2抢到了也没有办法执行test方法,因为t2拿不到锁就没有办法进到test方法中执行代码,这时候t2线程就会进入到锁池里面了。

注:java中任何对象都可以当做锁,能否拿到锁就决定了一个线程是否能进入到被锁的代码块中去执行代码


例如:
public class Test{
//默认使用this来充当这把锁,锁的是test1方法
public synchronized void test1(){
//代码
}
//默认使用this.getClass()来充当这把锁,锁的是test2方法
public static synchronized void test2(){
//代码
}

public void test3(){
//这种形式可以使用任意对象充当锁,锁的是这个代码块
synchronized(任意对象){
//代码
}
}
}

 

10.线程通信-wait()与notify()/notifyAll()方法
在Object中,三个wait方法(重载),notify()以及notifyAll()方法都是和线程通信有关的方法

注:这几个方法只能是在synchronized关键字使用时,被用来充当锁的对象才能调用,并且只能再加锁的范围内调用,否则其他情况调用会抛出异常。

1)wait方法
当一个线程拿到锁,进入到被锁的代码中执行代码时候,突然调用了锁的wait方法,那么这个线程这时候就交出CPU使用权,并且把锁方法原出,然后由运行状态进入到等待池中,进行"无限期"的等待,直到有其他线程对象调用了特定的打断/唤醒方法后,这个线程才能从等待池中出来。
注:如果使用有参数的wait方法,那就是"有限期"的等待

例如:
//默认使用this充当这把锁
public synchronized void test(){
int a = 1;
if(a>0){
try {
//只能使用锁调用wait方法
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


2)notify()方法
当一个线程在被锁的点中使用锁调用了notify()方法,那么可以随机唤醒在等待池等待这把锁的一个线程(如果有多个随机都等这同一把锁的话),这个被唤醒的线程就会从等待池进入到锁池中,如果之后某个时刻这个线程发现等待的池已经被其他线程释放了,那么它就会从锁池进入到就绪状态,准备争夺CPU的使用权并且争取这把锁。
例如:
//默认使用this充当这把锁
public synchronized void test(){
int a = 1;
if(a>0){
try {
//只能使用锁调用notify方法
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


3)notifyAll()方法
与notify类似,不同之处在于,notifyAll()方法会叫醒等待池中等待同一把锁的所有线程对象。
例如:
//默认使用this充当这把锁
public synchronized void test(){
int a = 1;
if(a>0){
try {
//只能使用锁调用notifyAll方法
this.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}




4)根据以下代码,补全pos和wit方法完成其功能
注:银行账号的余额不能是负数

public class Account {
//账号余额
private int balance;
public Account(int balnace) {
this.balance = balnace;
}
//存钱
public void pos(int money){

}
//消费
public void wit(int money){

}
}


//男孩,负责挣钱
public class Boy extends Thread{
private Account account;
public Boy(Account account, String name) {
this.account = account;
setName(name);
}
public void run() {
while(true){
int money = (int)(Math.random()*10000+1);
account.pos(money);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}


//女孩,负责花钱
public class Girl extends Thread{
private Account account;
public Girl(Account account, String name) {
this.account = account;
setName(name);
}
public void run() {
while(true){
int money = (int)(Math.random()*10000+1);
account.wit(money);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class AccountTest {
public static void main(String[] args) {
Account account = new Account(5000);
Boy boy = new Boy(account, "tom");
Girl lily1 = new Girl(account, "lily1");
Girl lily2 = new Girl(account, "lily2");
Girl lily3 = new Girl(account, "lily3");

boy.start();
lily1.start();
lily2.start();
lily3.start();
}
}


11.死锁
在程序中是不允许出现死锁情况,一旦发生那么只能手动停止JVM的运行,然后查找并修改产生死锁的问题代码。

简单的描述死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放。

注:可以通过jconsole查看到线程死锁的情况
例如:
public class ThreadDeadLock extends Thread{
private Object obj1;
private Object obj2;

public ThreadDeadLock(Object obj1,Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}

public void run() {
String name = Thread.currentThread().getName();
if("Thread-0".equals(name)){
while(true){
synchronized (obj1) {
synchronized (obj2) {
System.out.println(name+" 运行了..");
}
}
}
}
else{
while(true){
synchronized (obj2) {
synchronized (obj1) {
System.out.println(name+" 运行了..");
}
}
}
}
}

public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
Thread t1 = new ThreadDeadLock(obj1,obj2);
Thread t2 = new ThreadDeadLock(obj1,obj2);
t1.start();
t2.start();
}
}

 

posted @ 2019-09-07 00:06  蔡蔡的弱鸡  阅读(157)  评论(0编辑  收藏  举报