java 基础之多线程概述
  HvTJUzsxOBtS 2023年11月25日 24 0


####1、多线程的简介
#####1)在学习线程之前我们先了解一下进程:
(1)现在很多操作系统都支持多进程,我们可以单纯的理解为一个应用程序就是一个进程。比如:QQ,搜狗等
(2)实际上的多进程也不是多进程,比如表面上:我们一边听音乐,一边聊天,但实际上这些进程并不是同时进行的。
(3)在计算机上所有的进程都是由CPU执行的,对于CPU来说,某一个时间点只能运行一个进程。只是CPU在很短的时间内将运行的各个进程进行切换,由于CPU运行的很快,并且时间很短,才给人同时运行很多程序的感觉。

#####2)线程的了解

(1)在每一个运行的进程之中,都可以包含多个执行单元同时运行,这些执行单元可以看做是程序执行的一条条分流,被称为线程。
(2)每一个进程都至少含有一个线程,创建进程,会默认进入一个线程main()方法里面的代码。
(3)线程其实也是轮换执行的,这是CPU的执行机制决定的。

####2、线程的创建
线程的创建主要由两种方法:
1)继承java.lang包下的Thread类。
2)实现java.lang.Runnable接口。

他们都是复写run()方法来实现功能的实现。

#####1)Thread创建多线程的方法
大体思路:
(1)首先创建一个类MyThread继承Thread类:class MyThread extends Thread { }
(2)通过创建的MyThread类创建一个Thread对象 : Thread myThread = newThread();
(3)在MyTread里复写Thread()类里面的run方法,新线程实现的功能。
(4)开启线程:myThread.start()
#####2)实现Runable接口实现多线程
通过创建类来继承Threa具有局限性,因为类只能继承一个类,而接口却可以实现很多个。为了解决这种弊端,我们还有一种方法来创建一个新的线程,就是通过Thread(Rundble target)接口。
Thread(Runable target)接口只有一个run()方法,通过Tread(Runable target)构造方法创建线程的时候,只需要为它传递一个实现了Runable接口的实例化对象,就可以创建一个新的线程,并且运行新的run()方法,来实现新的功能。

大体实现思路:
(1)创建一个新的类MyThreadRunable接口Runable类 class MyThreadRunable implement Runable{}
(2)实例化继承类Runable对象:MyThreadRunable myRunable = new MyThreadRunable();
(3) 将实例化的Runable对象带入Thread(Runable target) 参数,创建新的线程:Thread myRunableThread = new Thread(myRunable);
(4)开启线程,myRunableThread.start();

主要就是Thread通过带入实例化对象myRnable 将线程开启的run方法,复写到Runable()的run方法里面。

####3、Thread和Runable的区别
他们主要的区别是对共有资源的调度上:
举个例子:卖车票
1)首先创建一个private int ticket =100;表示由100张票
2)用Thread创建四个线程表示四个窗口,然后获取ticket,对其做减法运算,类似卖出
3)打印log可以看到,实际上是四个线程都获取了一个ticket = 100,各自进行处理减法操作。
这在事实上是不合理的,因为一张票不可能同时卖出四个人。
4)用Thread(Runable target) 接口实现的四个线程操作,可以观察到
这四个线程是共同操作一个ticket = 100; 当线程1把ticket减去1 = 99之后,别的线程就会从99开始去减,而不是重新获取到一个ticket =100

关于Thread 、Thread(Runable target) 的实例请参阅:
java多线程---Thread和Runnable简单实例

####4、后台线程的概念
当一个线程添加setDaemon(true)语句之后就变成了后台线程:

········
myRunableThred.setDaemon(true);
myRunableThread.start();
··········

1)进程中只要还有前台进程,进程就不会结束。(新建的线程默认都是前台进程)
2)但是当进程中的线程都是后台进程的时候,进程就会结束。

####5、线程的运行周期
(1)线程的新建
(2)线程的就绪
(3)线程的运行
(4)线程的阻塞
(5)线程的结束
#####1)线程的新建
可以理解myThread.start();代码
#####2)线程的就绪
可以理解从可以理解myThread.start();代码到run(){}代码之间时间,等待CPU分配运行资源。
#####3)线程的运行
可以理解是run(){}方法运行之后
#####4)线程的阻塞
可以理解是线程运行的时候,停止了,导致阻塞的原因:
(1)当线程获取某个对象的同步锁的时候,锁被其他对象调用,还没有释放。
(2)当线程调用了阻塞的 IO方法时,比如管道,socket等
(3)当线程调用某个对象的wait()方法,就必须等到调用notify()才能唤醒。
(4)当线程调用Thread.sleep(long mills)方法,只有等到时间结束才会进入就绪状态。
(5)当线程通过join()方法调用其他的线程,需要等新加入的线程运行结束,才会进入就绪状态。

线程阻塞的例子:待续·····

#####5)线程的死亡
线程一般由三个方式死去:
1)运行完run()方法。
2)线程捕获到了异常(Exception)、错误(Error)。
3)使用thread.stop()来强行终止线程方法终止线程。

####6、线程的调度
1)线程的优先级
设置线程的优先级,优先级越高获得CPU执行的机会越大。
线程的优先级用1~10之间的整数表示,数字越大表示优先级越高。
除了直接使用数字表示优先级外,Thread还提供了三个静态常量来表示优先级:
Static int MAX_PRIORITY 表示线程优先级最高,相当于10
Static int NORM_PRIORITY 表示线程优先级普通,相当于5
Static int MIN_PRIORITY 表示线程优先级最低,相当于1

2)使用方法:

最高优先级

········
myRunableThred.setPriority(10);
myRunableThread.start();
··········

最低优先级

········
myRunableThred.setPriority(1);
myRunableThread.start();
··········

一般的操作系统,都会先去执行优先级较高的线程,然后再去执行优先级较低的线程。

####7、线程的休眠
当我们想让自己的线程暂停一段时间,等待一段时间,把CPU让给别的线程使用。我们可以调用sleep(long millis)方法。
该方法会在声明的时候抛出一个InterruptException异常,我们应该给以捕获,或者声明这个异常。
1)用法

········
try{
Thread.sleep(2000);//当前线程休眠2S
} catch  (interruptedException e){
    e.printStackTrace();
}
··········

请看清是当前线程,放在你要暂停的线程执行程序之中。

####8、线程使用yield()方法让步
当线程使用yield()时,线程就进入了就绪状态,等待系统调度器重新调度。
这个时候只有同级别的线程或者更高级别的线程才能够得到CPU 的执行。

1)用法
假设由Thread_one / Thread_two两个同级别线程

········
Thread_one.yield();//当前线程做出让步,同级别或者更高级别的线程去执行(Thread_two 去执行)
··········
········
Thread_two.yield();//当前线程做出让步,同级别或者更高级别的线程去执行(Thread_one去执行)
··········

####9、线程插队
类似与现实中插队一样,线程也会通过调用join()方法。插队遭到阻塞,只有当插队的线程运行结束之后,才能重新运行。
1)用法:
假设由Thread_one / Thread_two两个线程,
在线程一执行的代码中添加Thread_two.join();
就可以使得线程一得到阻塞,当线程2,运行完时,线程一才会继续运行。

####10、多线程同步
#####1)同步代码块
当多个线程操作统一资源的时候,往往会造成一些意外情况:
比如
1)四个Thread(Runable target)线程模拟售票,共ticket = 10张票。
2)线程执行run()函数中延时1S,模拟现实中的延时操作。
3)最后打印出来票数有可能ticket = 0;ticket = -1; ticket = -2;
为什么会导致这样的情况呢:
是因为当ticket = 1;的时候,比如线程1获取到了数据,但是此时线程是有一个延时的,线程一并没有把-1的操作及时返回给ticket.
这个时候线程2/3/4也是正在执行的,它们此时读到的ticket 的值也是ticket = 1; 也参与了 -1 的操作。所以最后ticket 会被多减好多次。

4)解决办法
(1) 就是让多个线程在访问共有资源的时候,一次对共有资源操作的那一块代码。每次只能有一个线程去操作。
这样也就类似于排队去执行共有资源,当一个线程操作完成之后另一个才能去操作。

(2) 我们采用同步机制,意思是共有资源对每个线程同时更新同步,我们使用synchronized(lock){
操作共享资源的代码快;
}

5)上面的lock是一个锁对象,当一个线程操作共享代码是被置0,此时别的线程无法进入操作共享资源的代码。当操作完成时,锁被置1,别的线程可以进行操作。

········
TicketCheck ticket = new TicketCheck();//创建TicketCheck 对象
new Thread(ticket,"线程1").start();
new Thread(ticket,"线程2").start();
new Thread(ticket,"线程3").start();
new Thread(ticket,"线程4").start();
·········

···········
class TicketCheck implements Runnable {
private int ticket = 10; //十张票
Object lock = new Object();//定义任意一个对象,作为同步代码的锁

public void run() {
while (true) {
synchronized (lock) {
				try{
				Thread.sleep(2000);//当前线程休眠2S
				} catch  (interruptedException e){
				    e.printStackTrace();
				}
				if (ticket > 0) {
				Log.v(TAG,"ticket:" + ticket--);
				} else {
				break;//跳出while循环,结束线程
				}
			}
		}//对应的synchronized代码块,要包含所有对共有资源的操作
	}
}
···········

#####2)同步方法
和同步代码块类似,同步方法可以理解为,把执行共有资源的操作的代码块放到了一个方法里面,然后用synchronized 来修饰这个方法。来达到每次只有一个线程访问执行共有资源的方法。

思路:
1)把每个线程操作共有资源的代码块放到一个函数里面。
2)用synchronized 在前面修饰这个函数。

········
TicketCheck ticket = new TicketCheck();//创建TicketCheck 对象
new Thread(ticket,"线程1").start();
new Thread(ticket,"线程2").start();
new Thread(ticket,"线程3").start();
new Thread(ticket,"线程4").start();
·········

···········
class TicketCheck implements Runnable {
private int ticket = 10; //十张票
Object lock = new Object();//定义任意一个对象,作为同步代码的锁

public void run() {
while (true) {
				saleTicket();//在run()方法里面调用saleTicket 方法,来对共有资源进行操作
				if (ticket < 0) {
				break;//跳出while循环,结束线程
				}
			}
	}
//用synchronized 修饰调用的方法表示每次只能一个线程调用
private synchronized void saleTicket() {
if (ticket > 0) {
				try{
				Thread.sleep(2000);//当前线程休眠2S
				} catch  (interruptedException e){
				    e.printStackTrace();
				}
			Log.v(TAG,"ticket:" + ticket--);
			} 
	}
}
···········

同步方法其实也是有锁的,就是调用同步方法的那个类,实例出来的对象。大多是this指向的对象,但是在静态方法中该对象是该方法所在的类的class对象,该对象可以直接用类名.class来获取。

####11、了解死锁现象
死锁现象,就类似于,A、B两个人手里拿着对方家门的钥匙,但是此时都是在自己家里面。所以自己出不去,对方也进不来。
代码实现:
1)建立两个锁
2)在run()方法里面,互相嵌套锁对象同步的代码;

········
TicketCheck ticket = new TicketCheck();//创建TicketCheck 对象
new Thread(ticket,"线程1").start();
new Thread(ticket,"线程2").start();

·········

···········
class TicketCheck implements Runnable {

static Object lock_A = new Object();//定义一个锁对象
static Object lock_B = new Object();//再定义一个锁对象
private boolean flag = true;


public void run() {
if (flag) {
flag = false;
while (true) {
synchronized (lock_A) {
		Log.v(TAG,"  A 锁"); //这个线程只能运行到这里
		synchronized (lock_B) {
		Log.v(TAG,"  B 锁");//打印不出来这一句,因为lock_B 已经被另一个线程占据
				}		
			}
		}//对应的synchronized代码块,要包含所有对共有资源的操作
		} else {
		flag = true;
		while (true) {
		synchronized (lock_B) {
		Log.v(TAG,"  B 锁");//这个线程只能运行到这里
		synchronized (lock_A) {
		Log.v(TAG,"  A 锁");//打印不出来这一句,因为lock_A 已经被另一个线程占据
				}		
			}
			}
		}
	}
}
···········

####12、线程之间的通信
当我们建立的线程对共有的一个事情分阶段操作的时候。
比如:A 线程存数据,B线程读数据。
我们期待A线程存好数据之后,B线程再度数据。
不然可能出现混乱的情况。
在此我们就可以使用wait()方法,和notify()、notifyAll()方法

1)void wait() 使当前线程放弃同步锁并进入等待,直到其它线程进入此同步锁,并且调用notify()方法,或者notifyAll()方法唤醒该线程为止。
2)void notify() 唤醒此同步锁等待的第一个调用wait()方法的线程。
3)void notifyAll()唤醒唤醒此同步锁上调用的wait()方法的所有线程。

需要注意的是这三个方法使用的必须是同一个同步锁对象,如果这三个不是同一个同步锁对象,会抛出IllegaMonitorStateException异常。

具体实例请参考:
线程--简单多线程通信实例

参考文档:
Java基础入门 传智博客高教产品研发部

本人郑重声明,本博客所著文章、图片版权归权利人持有,本博只做学习交流分享所用,不做任何商业用途。访问者可將本博提供的內容或服务用于个人学习、研究或欣赏,不得用于商业使用。同時,访问者应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人的合法权利;如果用于商业用途,须征得相关权利人的书面授权。若以上文章、图片的原作者不愿意在此展示內容,请及时通知在下,將及时予以刪除。


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月25日 0

暂无评论

推荐阅读
HvTJUzsxOBtS