目录
【资料图】
程序、进程和线程的概念
多线程的优点
Thread类关于多线程的创建
Thread类的相关方法
线程的调度
线程的五种状态
线程的同步
总结同步方法
衍生内容————单例设计模式
死锁问题
锁的概念
sleep()和wait()的异同
首先要明确几个概念
程序、进程和线程的概念
程序:完成特定任务,用某种特殊的语言编写的一组指令的集合
进程:是执行路径,一个进程同一时间并行或者正在运行的程序
线程:是执行路径,一个进程同一时间并行或者执行多个进程,就是多线程
注:进程中也有可能有多个线程
CPU也分为多核CPU和单核CPU
单核CPU:实际上进行的是某种意义上的假CPU,一个CPU同时做好多事,如果一个没有准备好,就先将该事件挂起,去进行别的,可以用一张图来表示
多核CPU(取决与主频来利用哪个):多核CPU就相当于多个单核CPU工作
同时也要解释两个词的含义
并行:多个CPU任务一起进行
并发:一个CPU做多个任务
注:并发只是看上去“同时”,但是实际上只是在CPU上进行高速的切换任务,以至于仅仅是看上去是同时,并行才是真正意义上的同时
多线程的优点
1、提高应用程序的响应
2、提高CPU的利用率
3、改善程序结构,每个线程独立运行,互不干扰,便于修改
提到多线程,就不得不提一个特殊的类
Thread类关于多线程的创建
方法一:
1、创建一个继承于Thread类的子类
2、重写Thread类中的run()
3、创建Thread类子类的对象(要在主线程上创建)、
4、通过对象去调用start()
想要创建一个多线程的代码如下
//主函数中的体现为//1、创建了继承Thread的子类//在继承Thread中的表现为public class ExtendsThread extends Thread { @Override //2、此处为标准的对于run()函数重写 //对run()函数的重写就相当于对于这一条线程中你想做的所有任务 public void run() { super.run(); for(int i=0;i<=20;i++) { System.out.println(i); } }}public class ThreadTest { public static void main(String[]args) { //3、创建了继承Thread子类的对象 Thread et=new ExtendsThread(); //4、通过对象调用了start() et.start(); //调用start()之后就开启多线程 }}
此处需要注意的是
1、run方法的重写:将这个线程要执行的所有操作全部都声明在run方法中
2、et.run()也能在主函数中直接调用,也能完整的执行在run方法中的指令,但是不能体现多线程,就仅仅是将指令完成,et.run()就仅仅只是调用方法看
3、不能够让已经start()的线程再去重启线程
4、可以创建多个对于ExtendsThread的对象,此时这个对象可以再次开始start(),相当于多开了一个线程,只不过执行的是相同内容
5、匿名子类与匿名对象同样适用
public class ThreadTest { public static void main(String[]args) { Thread et=new ExtendsThread(); //此处为体现多线程,同时开启两个线程 et.start(); //以下即为匿名子类 //直接开启多线程 new Thread(){ public void run() { super.run(); for(int i=0;i<=10;i++) { System.out.println(i+"#"+i); } } }.start(); }}public class ExtendsThread extends Thread { @Override public void run() { super.run(); for(int i=0;i<=10;i++) { System.out.println(i+"*"+i); } }}
第一次的执行结果
方法二:
1、创建一个实现了Runnable接口的类
2、实现Runnable接口中的抽象方法
3、创建实现类对象
4、将此对象作为参数传至Thread类的构造器,创造Thread类的对象
5、利用Thread()类的对象调用start()
public class RunnalbeThread implements Runnable//1、创建一个实现Runnable的类{ @Override//2、类中重写Runnable的方法,也就是run方法 public void run() { for(int i=1;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i); } }}public class ThreadTest { public static void main(String[] args) { Thread rt=new Thread(new RunnalbeThread()); //3、创建一个对应类的对象 //4、将这个对象传入到Thread的构造器 rt.start(); //5、用这个对应的Thread对象来继续调用start() rt.setName("线程3"); for(int i=1;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i+"-"+Thread.currentThread().isAlive()); } }}
在这个地方,如果没有创建匿名对象(对于实现Runnable的实现类),一个实现类的对象,可以多次传入到Thread的构造器里面,创造更多的线程
两种方法的比较
继承法(方法一)由于Java的单继承性,导致如果需要继承Thread类的类由原本的一套体系,可能会影响该代码的实现,由此看来,实现接口的方式是更加活泛的,更自由。
实操中优先选择Runnable接口的方式
1、实现的方式没有单继承性的限制
2、实现的方式更适合多个线程共享数据的情况
注:Thread类也实现了Runnable接口
Thread类的相关方法
1、String getName();
返回线程名称
2、void setName(String name);
设置线程名称
public static void main(String[] args) { Thread et = new ExtendsThread(); et.setName("线程--1"); System.out.printf(et.getName());}
运行结果
此处需要注意的是,主线程也是可以命名的,如以下代码
public class ThreadTest { public static void main(String[] args) { Thread et = new ExtendsThread(); Thread.currentThread().setName("主线程"); System.out.printf(Thread.currentThread().getName()); }}
运行结果如下
3、currentThread()方法
静态方法,返回当前执行此代码的线程(对象)
4、yield()方法
释放当前CPU的执行权
也存在当我们释放完执行权之后,CPU再次将执行权分配给目前线程的情况
5、join()方法
相当于在原本的线程1上,让另一个线程2截断,知道这个线程2执行结束,否则不再进行线程1(在线程1之中调用线程2的join方法)
代码测试如下
public class ExtendsThread extends Thread{ @Override public void run() { super.run(); for(int i=0;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i+"*"+i); } }}public class ExtendsThread2 extends Thread{ public void run() { super.run(); for(int i=0;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i+"#"+i); } }}public class ThreadTest { public static void main(String[] args) { Thread et = new ExtendsThread(); Thread et2=new ExtendsThread2(); et.start(); et2.start(); et.setName("线程1"); et2.setName("线程2"); Thread.currentThread().setName("主线程"); for(int i=0;i<=20;i++) { System.out.println(Thread.currentThread().getName()+":"+i); if(i%5==0) { try { et2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } }}
测试结果如下
当主线程的i跑到5的时候,此时调用了et.join()和et2.join()此时的主线程已经被挂起了,直到线程1和线程2运行完之后,才会继续主线程的进行。
6、stop()
强制结束线程,可以提前结束线程的生命周期。(不推荐使用stop()结束进程)
public class ExtendsThread extends Thread{ @Override public void run() { super.run(); for(int i=0;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i+"*"+i); if(i==5) { Thread.currentThread().stop(); //此处用stop强制停止了 //当i=5的时候强制停止线程 } } }}public class ThreadTest { public static void main(String[] args) { Thread et = new ExtendsThread(); Thread et2=new ExtendsThread2(); et.start(); et.setName("线程1"); Thread.currentThread().setName("主线程"); for(int i=1;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i); } }}
测试结果
如图所示,线程1确实只进行到i=5的时候
7、sleep(long millitime)
强制线程进入休眠,单位是毫秒
在指定时间内强制休眠
需要注意的是,对某个线程使用sleep的话,该线程就会进入到挂起状态,在指定时间挂起。相当于主动让出了CPU的执行权。
8、isAlive()
判断当前线程是否存活
举例如下
public class ExtendsThread extends Thread{ @Override public void run() { super.run(); for(int i=0;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i+"-"+Thread.currentThread().isAlive()); } }}public class ThreadTest { public static void main(String[] args) { Thread et = new ExtendsThread(); Thread et2=new ExtendsThread2(); et.start(); et.setName("线程1"); Thread.currentThread().setName("主线程"); for(int i=1;i<=10;i++) { System.out.println(Thread.currentThread().getName()+":"+i+"-"+Thread.currentThread().isAlive()); } System.out.println(et.isAlive()); }}
结果如下
如图所示,在代码的最后,et所开启的线程已经结束,所以此时打印出来的false
线程的调度
线程的进行主要是看时间片,一般情况下,多个线程都是并发,所以对于CPU的执行权一般是进行抢夺,高优先级的线程优先抢夺CPU的执行权。
说到这里就不得不提到线程的优先等级(这里的优先级都是在线程诞生的时候就是设置好的,默认为5)
>MAX_PRIORITY:10
>MIN_PRIORITY:1
>NORM_PRIORITY:5
也有两个方法是关于线程的优先级
1、getPriority():返回线程优先级
2、setPriority(int newPriority):改变线程的优先级
高优先级抢占低优先级的线程的CPU执行权,但是是从概率上而言,高优先级的线程有更大的概率去执行CPU
线程的五种状态
1、新建:当一个Thread类或其子类的声明并创建时,新生线程处于此状态
2、就绪:当线程被start()之后,就会进入队列等待CPU的时间片
3、运行:获得CPU资源,进入运行状态,run定义了线程操作和功能
4、阻塞:在某种情况下,被人为挂起或执行输入输出,让出CPU的执行权
5、死亡:线程完成了全部工作或被提前强制性中止(stop),或者出现异常导致结束,比如join()会使线程被挂起,造成线程阻塞
线程的同步
线程的安全问题(不一定出现线程安全问题)
没有sleep()出现时,错误的概率小,但是安全问题总是要解决的
有可能会出现极端情况
此时带入一个场景,比如说一个线程代表一个窗口,一个售票窗口,线程每进行一次就挂起一次,会打印票号,但是如果正常进行,票号应该是连号,但是会出现如下情况
代码如下
public class RunnalbeThread implements Runnable{ public static int num=30; public static int tnum=1; @Override public void run() { while(num!=0) { if(num>0) { num--; tnum++; System.out.println(Thread.currentThread().getName()+":"+tnum); try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }}public class ThreadTest { public static void main(String[] args) { Thread rt3 = new Thread(new RunnalbeThread()); Thread rt2 = new Thread(new RunnalbeThread()); Thread rt = new Thread(new RunnalbeThread()); rt2.start(); rt3.start(); rt.start(); rt3.setName("线程3"); rt.setName("线程1"); rt2.setName("线程2"); }}
代码测试结果如下
很明显的,会出现重号的现象
原因:当某个线程操作票的过程中,尚未完成操作,另一个线程参与进来,也对车票进行操作(相当于是共享数据)
如何解决
加锁
当一个线程在操作共享数据的时候,其他线程不能参与,直到线程a操作结束,其他线程才能开始操作。即使a处于阻塞状态,也不能被改变
方法一:同步代码块
synchronized(同步监视器){
需要被同步的代码}
说明:操作共享数据的代码,即为需要被同步的代码
同步监视器,俗称锁,可以随意扔一个对象进去
要求:多个线程要共用同一把锁,不能设置多个锁,此时不能使用匿名
缺点:操作同步代码时,仅能有一个线程操作,其他的都在等待,相当于是一个单线程操作过程,相对而言效率会很低
此时会出现一个锁不唯一的问题,由于锁的创建在Thread的子类中,但是使用此方法创造进程需要newThread的子类的对象,此时会new出很多锁,此时最好的解决方案就是把锁进行static
方法展示
public class RunnalbeThread implements Runnable{ public static int num=30; @Override public void run() { while(num!=0) { try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (RunnalbeThread.class) { if(num>0) { num--; System.out.println(Thread.currentThread().getName()+":"+num); } } } }}public class ThreadTest { public static void main(String[] args) { Thread rt3 = new Thread(new RunnalbeThread()); Thread rt2 = new Thread(new RunnalbeThread()); Thread rt = new Thread(new RunnalbeThread()); rt2.start(); rt3.start(); rt.start(); rt3.setName("线程3"); rt.setName("线程1"); rt2.setName("线程2"); }}
代码中,把对于所有共享数据的操作全部都包起来了,达到监视的作用
结果如下
还有一个需要注意的点就是如果是用接口实现的方法创建的线程,可以考虑使用this的,之所以继承法不能使用,是因为其依靠创造他本身的对象来创造线程,但是实现类只创造一个对象,其他对象都是利用Thread进行创造的。
但是我的代码中,监视器之后的锁就不能使用this,因为在主函数中,我用的创建方法并不是一个对象传入到Thread的构造器中,我使用了匿名对象,如果使用this,每一次的锁都是不一样的锁,无法起到监视作用了
同时,在我的代码中,使用了synchronized (类名.class)这种方式,在这里需要注意的是,类本身也是一个对象,类仅加载一次,与每次new完之后出现的新对象不同。所以在我看来,类是一个完美的锁,不会出现重复的现象。
也需要注意对于同步代码的包装。要注意包装的范围,少包不能解决安全问题,包多了会影响效率,而且也容易出现新的问题。
方式二:
1、同步方法实现Runnable接口
synchronized可以修饰方法,但是需要符合题意,一般情况下不建议使用
在同步方法的内部,就和使用synchronized包起来是一个效果
使用同步方法时,同步监视器就是this
2、同步方法继承Thread类的方法
对于继承法而言,很明显不能直接加synchronized,加了synchronized之后,会自动使用this作为监视器,很显然不行,此时应该将方法改成静态
总结同步方法
1、仍涉及同步监视器,只是不需要显式声明
2、非静态的同步方法是this,静态方法的监视器视为当前类本身
衍生内容————单例设计模式
1、懒汉式(线程安全)
先来分析一下,在原本对于懒汉式的代码中,线程安全可能会出现的部位
public class Bank { private Bank(){} private static Bank instance=null; public static Bank getInstance() { if(instance==null) { instance=new Bank(); } //在此段就容易出现堵塞或者就绪,当多线程在此处参与时,设线程a、线程b //a判断了instance==null,已经进入了语句,此时CPU将执行权切换给了b或 //a由于某种原因阻塞了,那么此时可能就不仅仅创建了一个对象 return instance; }}//而在关于单例式操作,同时满足有多个线程,有共享数据这两个条件,可以实现线程安全
本质上就是线程a、b抢锁,谁先抢到就谁先造
如果想用同步方法,在本例中就可以直接将getInstance这个方法直接使用synchronized直接修饰,就可以解决线程安全问题
如果想使用同步代码块,就可以使用synchronized将getInstance这个方法中的内容直接包裹,并且利用Bank.class对代码进行监视(效率差)
同步代码块——方法一
public class Bank { private Bank(){} private static Bank instance=null; public static Bank getInstance() { if(instance==null) { synchronized(Bank.class) { instance=new Bank(); } } return instance; }}
同步代码块——方法二
public class Bank { private Bank(){} private static Bank instance=null; public static Bank getInstance() { synchronized(Bank.class) { if(instance==null) { instance=new Bank(); } } return instance; }}
两个方法在使用上的区别不大,都可以正常使用,但是实际上方法一的效率更高
假设现在有线程1和线程2,当线程1率先抢到CPU控制权,先制造了对象,线程2在方法二中仍停留在synchronized语句上等待,一直到线程1制造完对象,线程2才能够进入if,判断失败之后离开该方法,但是在方法一中,线程2先进入判断,如果1已经造完对象了,那么线程2就会直接离开。线程2就不会再进入等待区。
死锁问题
不同的线程分别占用了对象所需资源不放,都在等对方放弃,形成死锁
>不出现异常,不出现提示,所有的线程阻塞,不再进行
使用同步的时候,一定要避免死锁问题出现
锁的概念
Lock实际上就是一个接口,需要有实现类
Lock接口的具体使用,主要是对其实现类:Reentrantlock的使用
Reentrantlock
这个类有两个构造器,有一个形参fair
如果fair是true,就遵循先入先出,按照abc顺序开锁
如果fair是false或者没有参数,那么就是abc抢锁,谁先抢到谁先开
1、实例化Reentrantlock
2、将同步代码放到try中,在try首行调用Reentrantlock的对象调用Lock(),也可以调用解锁,try-finally,其中不使用catch,只是想让finally无论如果先给Lock解锁,即使try过程有异常,也会给Lock解锁
(其实本质上也就是上锁,只不过Lock需要手动开锁,但是synchronized不需要,synchronized自动就会开锁)
synchronized和Lock的异同
synchronized机制在执行完同步代码块后自动释放同步监视器
Lock需要手动开锁,不然会一直锁定一个线程不放
基本都会使用synchronized,但是实际上更建议使用Lock
sleep()和wait()的异同
相同:都可以使当前线程进入阻塞
不同:
1、两个方法声明位置不同,Thread类中声明sleep(),Object类中声明wait()
2、调用范围不同,sleep()在任何场景都能调用,wait()必须使用在同步方法或者同步代码块中
3、关于是否释放同步监视器,如果二者都在同步中,sleep()不释放锁,但是wait()会释放锁