admin 管理员组文章数量: 1087649
回顾多线程基础
首先我们要明白什么是进程,什么是线程?
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
什么是多线程?
- 1.多进程是指操作系统能同时运行多个任务(程序)。
- 2.多线程是指在同一程序中有多个顺序流在执行.
- 3.很多线程都是模拟出来的,真正的多线程指的是多个cpu,即多核,如服务器。
- 4.如果是模拟出来的多线程,即在同一个cpu的情况下,cpu只能执行一个程序或一行代码,因为切换的很快,所以有了同时执行的错觉。
- 5.在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
线程的生命周期为什么?
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
我们之前在多线程之前学习的多线程基础基本上都是单线程执行的
普通方法调用和多线程
多线程核心概念
实现多线程的几种方式:
- 继承Thread类:
首先,我们看JDK1.8的帮助文档对Thread类的描述:空线是一个线程在执行一个程序。java虚拟机的应用程序可以有多个同时执行的线程。 每一个线程都有一个优先权。具有更高优先级的线程在优先于较低优先级的线程中执行。每一个线程可能会或可能不会被标记为一个守护进程。在一些线程中运行的代码创建了一个新的Thread对象时,新线程的优先级被设置为等于创建线程的优先级,是守护线程的当且仅当创建线程是一个守护进程。
让我们来写一个例子:
public class TestThread extends Thread{private String url;//文件地址
private String fileName;//文件名public TestThread(String url, String fileName) {this.url = url;this.fileName = fileName;
}@Overridepublic void run() {WebDownLoader webDownLoader = new WebDownLoader();webDownLoader.DownloadPicture(url, fileName);System.out.println("下载了文件:"+fileName); }public static void main(String[] args) {TestThread testThread = new TestThread(";quality=80&size=b9999_10000&sec=1606911965336&di=b68b28cd8bd82094ddfd3f9ecfd380e4&imgtype=0&src=http%3A%2F%2Fimage.namedq.com%2Fuploads%2F20181006%2F19%2F1538823821-QrIiEnXmAv.jpg", "亚索");TestThread testThread2 = new TestThread("=2106604486,285772919&fm=26&gp=0.jpg", "光辉");testThread.start();testThread2.start();}}//创建一个下载文件的类class WebDownLoader{//创建一个下载文件的方法
public static void DownloadPicture(String url,String fileName) {try {FileUtils.copyURLToFile(new URL(url), new File(fileName));} catch (MalformedURLException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}
}
运行后:发现在项目中会多出两个下载的文件。
序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,两条线程理论上就同时去下载两个文件。
注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
- 实现Runnable接口
首先,让我们看看JDK1.8帮助文档对Runnable接口的描述:创建一个线程是声明一个类实现Runnable接口的其他方式。该类实现run方法。那类的一个实例可以分配,作为一个参数传递Thread时创建,并开始。此外,Runnable一类是主动的而不是子类Thread提供手段。一个类实现Runnable可以运行没有子Thread实例化一个Thread实例并将自身作为目标。在大多数情况下,这Runnable接口应该是如果你只打算重写run()方法并没有其他Thread方法。这是重要的因为方法重写,继承类不应该继承除非程序员打算修改或增强类的基本行为。
让我们来写一个例子:
/*** 演示线程的停止:1.创建线程,然后一直让它运行,通过我们自定义的方法让线程停止* 2.不建议使用JDK中的自带的API方法来让线程停止,因为可能会存在某些问题(官方推荐我们使用:自定义一个标志位来让某个线程进行停止)* **/
public class TestThreadStop implements Runnable{private static boolean flag=true;@Overridepublic void run() {while (flag) {System.out.println(Thread.currentThread().getName()+"-->循环");} }void testflag(){flag=false;System.out.println("自定义线程停止");}public static void main(String[] args) {TestThreadStop testThreadStop = new TestThreadStop(); new Thread(testThreadStop).start();//主线程执行for循环for (int i = 0; i < 1000; i++) {if (i==900) {//让我们自定义的线程停止testThreadStop.testflag();}System.out.println("主线程执行--》"+i);}}
}
输出结果:
当自定义线程输出到达900时候,线程会根据标志位停止,线程执行完毕。主线程继续执行。
- 实现Callable接口
首先,让我们看看JDK1.8帮助文档对Callable接口的描述:返回结果的一个任务,并可能抛出异常。用户定义一个不带参数调用 call 的Callable接口类似于Runnable,,是专为其实例的类可能被另一个线程执行。然而,Runnable,不返回结果并不能抛出异常。
Executors类包含的实用方法与其他常见的形式转换为Callable类。
让我们来写一个例子:
public class TestCallable implements Callable<Boolean>{@Overridepublic Boolean call() throws Exception {System.out.println("继承Callable接口的线程"+Thread.currentThread().getName()+"执行");return true;}public static void main(String[] args) {//1.创建两个继承了Callable接口的类的线程对象TestCallable testCallable = new TestCallable();TestCallable testCallable1 = new TestCallable();//2.创建线程池服务,线程池最大为2个线程ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();//3.往线程池中提交线程,并获取到线程执行call方法结果Future<Boolean> submit = newCachedThreadPool.submit(testCallable);Future<Boolean> submit2 = newCachedThreadPool.submit(testCallable1);//4.获取接口try {Boolean boolean1 = submit.get();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (ExecutionException e) {// TODO Auto-generated catch blocke.printStackTrace();}try {Boolean boolean2 = submit2.get();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (ExecutionException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
输出结果:
那我们之前学习了继承Thread类和实现Runnable接口来创建线程,这两个方式的区别是什么?
- 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。所以Runnbale比Thread跟具有优势
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
线程状态转换(线程的生命周期)
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程的调度
- 线程睡眠:Thread.sleep(long millis)方法
让我们来写一个例子:
public class TestThreadSleep {private static int mun=10;//模拟倒计时方法public static void tenDown() throws InterruptedException{while (mun>0) {Thread.sleep(2000);System.out.println(mun--);} }//打印当前时间方法private static void datedayin(){while (true) {Date date = new Date(System.currentTimeMillis());//获取当前时间try {Thread.sleep(1000);//睡眠一秒} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("当前时间是:"+new SimpleDateFormat("HH:mm:ss").format(date));date = new Date(System.currentTimeMillis());//更新当前时间}}public static void main(String[] args) {//模拟倒计时/*try {tenDown();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}*///打印当前时间datedayin();}
}
输出结果:
注意:wait和sleep的区别:Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
2.线程礼让:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
3.线程强制执行:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
让我们写一个例子:
/*** * 演示java线程中的join方法:使某个线程进行插队,直到这个线程执行完后,其他线程才能执行**/
public class TestJoin implements Runnable{@Overridepublic void run() {for (int i = 0; i < 200; i++) {System.out.println("大哥线程来执行了:"+Thread.currentThread().getName()+"-->"+i);}}public static void main(String[] args) {//创建自定义线程TestJoin testJoin = new TestJoin();Thread thread = new Thread(testJoin);thread.start();//主线程执行for循环,到200的时候 join方法让自定义线程进行插队,直到自定义线程执行完后,主线程才继续执行//(如果电脑是多核的,有多个cpu,可能在200之前,两个线程会交替执行)for (int i = 0; i < 500; i++) {if (i==200) {try {thread.join(); //进行插队} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}System.out.println("执行main方法:-->"+i);}}}
输出结果:
当主线程达到200的时候,自定义线程使用join方法进行插队,直到自定义线程执行完毕为止,主线程才继续执行。
3.调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
注意:每个线程都有默认的优先级。主线程的默认优先级Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
让我们来写一个例子:
/*** * 多线程的优先等级:1-10* 1 最低* 5 默认* 10 最高*/
public class 多线程的优先级 {public static void main(String[] args) {// TODO Auto-generated method stub
//获取多线程的优先级等级//1.最高优先级System.out.println(Thread.MAX_PRIORITY);//10//2.默认优先级System.out.println(Thread.NORM_PRIORITY);//5//3.最低优先级System.out.println(Thread.MIN_PRIORITY);//1Thread t1=new projuct();//获取线程Thread t2=new projuct();t1.setName("T1");//修改线程名t2.setName("T2");//查看线程优先级,自定义的线程没有设置前都是默认的优先级 5System.out.println(t1.getPriority());//5System.out.println(t2.getPriority());//5//设置优先级t1.setPriority(4);t2.setPriority(10);//执行线程t1.start();t2.start();}}
//定义一个线程
class projuct extends Thread{public void run(){for(int i=0;i<30;i++){System.out.println(Thread.currentThread().getName()+"-->"+i);//获取当前的线程和名称}}
}
输出结果为:
会发现在抢夺CPU调度的过程中,T2线程因为优先级的原因抢夺的CPU时间片会多一些。
守护线程
让我们来写一个例子:
public class 学习守护线程 {
/*** 守护线程* 其他所有线程结束,则守护线程退出* 守护线程一般都是无限执行的* */public static void main(String[] args) {// TODO Auto-generated method stub
Thread t1=new guard();
t1.setName("t1");
t1.setDaemon(true);//设置t1为守护线程
//知道主线程结束 它才会结束
t1.start();for(int i=0;i<10;i++){System.out.println(Thread.currentThread().getName()+"-->"+i);try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}
}}}
class guard extends Thread{public void run(){int i=0;while(true){i++;System.out.println(Thread.currentThread().getName()+"-->"+i);try {Thread.sleep(500);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
}
输出结果:
线程的同步
首先,我们需要理解一个概念:什么是线程的同步?线程的同步就是多个线程同时操作同一个资源。
首先,我们可以看一个多个线程同时操作一个共有资源的例子,会发现出现很多bug和漏洞:
public class TestTicket implements Runnable{//总共有10张票private int Ticket=10;@Overridepublic void run() {while (true) {if (Ticket<=0) {break;//当票被抢完的时候,就退出循环} try {Thread.sleep(100);//每当一个线程抢完一次票的时候,就让线程暂停0.2秒} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}//Thread.currentThread():返回当前线程System.out.println(Thread.currentThread().getName()+"抢到了第"+Ticket--+"票");//每当一个线程抢到一张票的时候,票数资源就-- }}public static void main(String[] args) {//创建线程TestTicket testTicket = new TestTicket();new Thread(testTicket,"小明").start();new Thread(testTicket,"老师").start();new Thread(testTicket,"黄牛党").start();} }
结束输出:
这肯定是不允许的,事实上是不允许几个同时抢到同一张票的,这时候出现了数据混乱。
这时候我们就可以使用synchronized关键字
我们在上面的例子中加入synchronized锁:
public class TestTicket implements Runnable{//总共有10张票private int Ticket=10;@Overridepublic void run() {synchronized (this) {while (true) {if (Ticket<=0) {break;//当票被抢完的时候,就退出循环} try {Thread.sleep(100);//每当一个线程抢完一次票的时候,就让线程暂停0.2秒} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}//Thread.currentThread():返回当前线程System.out.println(Thread.currentThread().getName()+"抢到了第"+Ticket--+"票");//每当一个线程抢到一张票的时候,票数资源就-- }}}public static void main(String[] args) {//创建线程TestTicket testTicket = new TestTicket();new Thread(testTicket,"小明").start();new Thread(testTicket,"老师").start();new Thread(testTicket,"黄牛党").start();} }
synchronized关键字有什么用?
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
3) 除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;
4) synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
什么是死锁?
简单来说:就是两个线程各有一把锁,但是又想要对面手中的锁,但是又不肯把自己的锁给对方,这时候程序就无法继续向下执行。
/*** 演示死锁的概念:当我们去获取到一个对象的锁之后,在这个锁还没有执行完毕的时候去获取到另一个对象的锁(关键是另一个对象的锁还在别的线程的手中)* 这就会造成死锁,程序无法向下执行。* 或者说:当我们去获取到一个对象的锁之后,在这个锁还没有执行完毕的时候去获取到另一个对象的锁(关键是另一个对象的锁还在别的线程的手中,并且别的线程这时候也想* 获取到我们手中对象的锁,这样就会僵持不下,程序无法向下执行,直接卡死)* **/
/*** 演示:1.创建两个小孩对象,小孩1获取到玩具车对象的锁,这时候想去获取到小孩2手中玩具飞机对象的锁* 2.小孩2获取到玩具飞机对象的锁,这时候象去获取到小孩1手中玩具车对象的锁* 3.这就造成了死锁状态,程序无法向下执行了*///创建玩具车类
class Car{}
//创建玩具飞机类
class Aircraft{}public class TestDieLock extends Thread {static Car car=new Car();
static Aircraft aircraft=new Aircraft();private String child;//小孩名public TestDieLock(String child) {this.child = child;
}public void run() {try {wanju();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}public void wanju() throws InterruptedException {if (child.equals("小孩1")) {synchronized (car) {System.out.println("小孩1获取到了玩具车的对象锁");Thread.sleep(1000);synchronized (aircraft) {System.out.println("小孩1还想获取玩具飞机的对象锁");}}}else {synchronized (aircraft) {System.out.println("小孩2获取到了玩具飞机的对象锁");Thread.sleep(1000);synchronized (car) {System.out.println("小孩2还想获取玩具车的对象锁");}}}}public static void main(String[] args) {TestDieLock testDieLock = new TestDieLock("小孩1");TestDieLock testDieLock2 = new TestDieLock("小孩2");testDieLock.start();testDieLock.start();
}
}
Lock锁
让我们来写一个例子:
/*** 在JDK5.0的时候推出了一个可重入锁:Lock,这个锁可以作用和对象锁相同,但是这个锁需要自己去加锁和解锁,通常我们使用try catch来让我们的方法或者代码块* 进行锁机制**/
public class TestLock implements Runnable{
int num=10;//定义可重入锁对象
private static ReentrantLock reentrantlock=new ReentrantLock();@Overridepublic void run() {while (true) {try {//调用可重入锁对象加锁方法,给下面代码块加锁reentrantlock.lock();if (num>10) {System.out.println(num--);try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();} }else {break;}}finally {//使用可重入锁,别忘记在finally中解锁reentrantlock.unlock();}}}public static void main(String[] args) {TestLock testLock = new TestLock();new Thread(testLock).start();new Thread(testLock).start();new Thread(testLock).start();}}
本文标签: 回顾多线程基础
版权声明:本文标题:回顾多线程基础 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1686651916a20574.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论