java多线程基础
进程和线程
首先进程和线程都是一个过程
进程:进程就是一个程序开启后到结束的过程
程序是静态的代码,运行后加载到内存就是开启了进程,进程就是这个程序运行的整个生命周期
线程:线程由进程创建,一个进程可以创建多个线程,一个线程又可以创建多个线程
线程更像是一个 程序的功能(部分代码) 执行过程,这个功能运行的生命周期就是线程,只是线程由进程创建,有时一个功能(线程)开启多次,有时开启多个不同功能(线程)
举个栗子:
硬盘上保存的迅雷代码就是程序,打开迅雷就是开启了一个进程,同时下载多个资源就是开启多个下载线程,同时又在上传资源,下载和上传是两个不同的线程,也就是不同的功能,他们都是由迅雷这个进程创建的
并发和并行
并行和并发是个相对概念,要看程序运行环境(几核CPU),和参照物(其他程序)
并发:一段时间内多个程序交替执行,但是因为这段时间很短,所以给人两个进程,或者两个线程同时运行的"错觉",但在某一刻只运行了一个程序
并行:某一刻两个程序同时运行,并非交替而是都在同时执行
举个栗子:
1.单核cpu在一个时间段内同时执行QQ和微信,对于QQ和微信而言就是在并发
2.双核cpu同一时刻,一个执行QQ一个执行微信,某一刻QQ和微信就是在并行
3.双核cpu,cpu1执行QQ和微信,cpu2执行迅雷和百度
环境为单个cpu时,cpu上运行的两个程序就是并发
环境为两个cpu时,同一时刻两个cpu运行的不同的两个进程就是并行
所以环境为两核cpu,参照物是四个进程时,对于这四个进程而言,并发和并行同时存在,只是在不同参照物之间
4.环境为两核cpu,cpu1执行QQ,cpu2执行微信,某一刻QQ和微信并行,然后cpu1执行聊天线程,cpu2执行视频通话线程
某一刻QQ聊天的线程和微信视频通话线程就在并行
如果cpu1一段时间执行多个聊天线程,对于多个聊天线程就是在并发
这就是:并行和并发是个相对概念,要看程序运行环境(几核CPU),和参照物(那些程序)
=========================================================================
小知识:通过java程序查看自己电脑是几核的处理器,两个cpu一个cpu是四核,那就是八核处理器
Runtime是一个单例模式的类,通过类方法获取对象
Runtime.getRuntime()
对象.availableProcessors() //获取处理器核心数量
Runtime r=Runtime.getRuntime();
int count=r.availableProcessors();
System.out.println(count);
/*
获取的是几核,而不是几个cpu
*/
=========================================================================
创建线程:
创建线程有两种方式:
1.继承Thread类,重写run()方法
2.实现Runnable接口,实现run方法(推荐)
继承Thread类实现线程
run()方法就是写线程功能的地方
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 80; i++) {
System.out.println(Thread.currentThread().getName());
try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
调用线程:
1.创建线程对象
MyThread mythread=new MyThread();
2.启动线程:
对象.start()
start()方法的底层是调用的start0(),这是一个native方法c/c++实现的,该方法会将线程创建后交给系统,具体如何执行就要看系统的调度算法了。
mythread.start();
3.通过Terminal开启jconsole,监视管理进程的控制台
=========================================================================
当执行main方法 或 执行单元测试,进程会创建一个main线程(主线程),因为自定义线程是在
main方法/单元测试中创建,所以main线程会在创建自定义线程Thread-0。
两个线程不属于所属关系 (虽然Thread-0是main创建的) ,而是并存,如果是单核cpu两个main线程和Thread-0线程就是并发,如果是多核处理器,main和Thread-0就是并行。
当main执行结束后,Thread-0没执行完,会继续执行,只关闭执行完毕的main线程。这就是并存,而非所属。
JUnit 测试方法执行完了,整个测试进程可能会被JUnit框架强制结束,从而中断所有未完成的线程(包括Thread-0)!
在正常的
main()
方法中,JVM 会一直等所有非守护线程执行完,才退出。所以测试线程最好不要使用单元测试!
=========================================================================
//单元测试
@Test
public void testone() throws IOException, InterruptedException {
MyThread tt=new MyThread ();
mythread.start();
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
//自定义线程
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 80; i++) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
该单元测试Thread-0线程中循环执行30次后整个进程就结束了,Thread-0也结束了,就是因为Junit单元测试main线程结束后,Junit会强制执行整个进程。
2.实现Runnable创建线程(真正开启线程的是Thread对象):推荐使用
有的类继承了其他类就不能继承Thread,但是还想要实现线程,就只能通过实现Runnable接口来实现线程的创建。
实现原理:Runnable没有start()方法,所以不能直接使用实现Runnable接口的类对象去调用start()。这时就要通过Thread对象作为静态代理,通过执行Thread对象的start()方法去开启线程。
Thread类中有一个Runnable类型变量,通过传参给该变量赋值,start()方法中执行的run()方法就是Runnable类型变量的run()方法
2.1:实现Runnable接口
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 80; i++) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2.2:通过Thread对象开启线程:
MyThread mythread=new MyThread();
Thread thread=new Thread(mythread);
thread.start();
=========================================================================
窗口售票问题:
1.当开启多个同一功能的售票线程,很容易就会多买出几张票,这是因为没有同步锁,在count为2或1时,三个线程都拿到了count做了--,所以就会导致多买出几张票
public class MyThread implements Runnable{
private static int count=100;
private boolean loop=true;
@Override
public void run() {
while(loop){
System.out.println("窗口:"+Thread.currentThread().getName()+"余票:"+(--count));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(count<=0){
break;
}
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
三个线程同时执行同一功能模块,由部分执行结果可知,多卖出一张票
MyThread myThread=new MyThread();
new Thread(myThread).start();
new Thread(myThread).start();
new Thread(myThread).start();
/*
窗口:Thread-2余票:3
窗口:Thread-0余票:-1
窗口:Thread-1余票:0
*/
2.如果想要主动结束某一个循环线程可以通过设置boolean变量通过setter方法结束线程循环(不常用知道即可),一般通过interrut()中断程序。
MyThread myThread=new MyThread();
new Thread(myThread).start();
Thread.sleep(3*1000); //这里是mian线程挂起3s
myThread.setLoop(false); //执行完这一句main线程才会结束
如果3s内myThread线程运行完毕,会先关闭,等执行完myThread.setLopp(false)时main线程才会结束。
=========================================================================
线程对象常用的方法:只有Thread对象和它的子类对象才有设置现成的方法
实现接口Runnable的类并没有线程方法,因为真正开启线程的类还是Thread,并非实现Runnable的类。
setName() //设置线程名
getName() //返回线程名String
setPriorty() //设置优先级,Thread.NORM/MAX/MIN_PROORTY三个参数优先级
getPriorty() //返回优先级的值int
//该方法是将中断标志设置为true,告诉线程建议你中断。
情况1.如果线程正处于挂起状态(sleep/wait/join)才会相应抛出异常,并挂起线程。
情况2.没有挂起只是将标志设置为true,并不会中断线程
interrupt()
//检测现成的中断标志为true还是false,可以根据该值来进行下一步操作,和interrupt()一起使用
.isinterrupted()
//join()方法只会挂起创建他的父线程,并不会影响其他线程的执行⭐⭐⭐⭐⭐
//仅挂起父线程,只有本线程执行完毕,才会解开父线程的挂起,或者通过父线程的interrupt()方法打破挂起,通过异常处理继续循环
join() /join(long time)
//释放线程占有的锁(也就是释放资源).让本线程挂起
wait()/wait(long time)
//将线程设置为守护线程 ,默认是false用户线程,参数给true则设置为守护线程
用户线程,就是进程创建的主线程和其下创建的其他子线程,都是用户线程
守护线程:守护线程就多一个特性:用户线程全部结束了,jvm会关闭所有运行的守护线程,其他和普通线程一样使用。像垃圾回收机制,就是一个典型的守护线程
setDaemon(true) 一般用于监听和获取其线程信息
Thread类的静态方法:
Thread.sleep(long n) //挂起当前进程n毫秒
注*sleep()方法只能挂起该语句当前所在线程,哪怕使用自己创建的Thread对象或者Thread的子类对象调用sleep一样是调用的类的静态方法,挂起语句所在的线程。⭐⭐⭐⭐⭐
MyThread myThread=new MyThread();
Thread thread = new Thread(myThread);
thread.start();
thread.sleep(1000*10);
Thread.sleep(10*1000);
代码中thread.sleep()调用的仍然是Thread的静态方法sleep(),所以挂起的不是thread线程,而是主线程
注*如果想挂起某个线程只能再run()方法中进行挂起,不可能通过线程对象的sleep()方法挂起,因为通过调用对象的sleep()方法仍然时调用的Thread的静态方法,挂起当前线程。⭐⭐⭐⭐
Thread.interrupted() //将中断标志设置为false,取消中断
//告诉系统的线程调度器,该线程愿意让出cpu时间片,具体然不让由系统决定,资源不紧张时,是不会让的
Thread.yield()
线程生命周期中的七大状态:
首先要说明几个概念:⭐⭐⭐⭐⭐
锁(Synchronized):是一种机制,保护对象锁/类锁下的代码在某一刻只允许一个线程访问,持有锁就是拥有了运行该锁下面代码的权利
阻塞:争抢带锁资源失败就会被阻塞,因为没有拿到锁,所以没有持有任何资源
挂起:当wait() sleep() join() 都是进入挂起状态,只有wait()挂起才会释放锁,sleep()和join()都不会释放锁
┌──────────────┐
│ New 新建
└──────┬───────┘
│ start()
▼
┌──────────────┐
│ Runnable 就绪
└──────┬───────┘
│ CPU调度
▼
┌──────────────┐
Running 运行中
sleep() wait() 阻塞(抢锁失败)
▼ ▼ ▼
┌────────────┐┌────────────┐┌────────────┐
│ TimedWait ││ Waiting ││ Blocked │
│ (sleep等) (wait等) │(锁阻塞) │
└────┬───────┘└────┬───────┘└────┬───────┘
│ 超时/唤醒 │ 被唤醒 │ 获取到锁
▼ ▼ ▼
┌──────────────┐
│ Runnable 就绪
└──────┬───────┘
│ CPU调度
▼
┌──────────────┐
│ Running 运行中
│
▼
┌──────────────┐
│ Terminated终止
└──────────────┘
七种状态:
New:new一个线程,该线程就会进入New状态
Runnable分为两个状态,Ready(就绪)和Running(运行)
Ready:当阻塞和挂起的线程自动或被动达到运行条件就会进入Ready状态,准备被调度器调度
Running:当线程被真正的运行就是此状态
Blocked:没有获取到锁(Synchronized),进入的阻塞状态
Waiting: 没有时间限制的挂起状态(sleep()/wait()/join())
TimeWait:有时间限制的挂起状态(sleep(time)/wait(time)/join(time))
Terminated:线程结束后就是这个状态
Synchronized:开启同步机制,是一种互斥锁
同步机制本质上锁住的是锁住某块代码,参数只是一个锁的唯一表示,并不参数代码的调用
(为什么用对象做标识,因为对象是唯一的还能自定义所以拿来做标识,参数之所以分为对象/Class都对象就是用来区分锁住的内容是普通代码还是静态代码,这决定后面的调用方式???????????个人理解不一定对)
因此Synchronized就分为对象锁和类锁:
对象锁:
1.在方法上直接使用Synchronized,默认标识是this
2.在普通方法中使用同步代码块Synchronized(Object obj){},标识是obj
注*对象锁所在的类,创建的每个对象中都会又这个锁,线程访问不同对象下同一段带锁的代码,获取的锁不同,不会引发争抢⭐⭐⭐⭐⭐
锁类:
1.在静态方法上使用Synchronized,默认标识是类名.calss
2.在静态方法中使用静态同步代码块Synchronized(类名.class){}
注*类锁所在的类,创建的所有对象中公用一个锁,只要是访问该对象下同一标识锁下的代码,就会申请同一把锁⭐⭐⭐⭐
线程获取对象锁和类锁是的不同:(核心⭐⭐⭐⭐⭐)
1.获取对象锁:该类中的所有锁都会在实例化的对象中创建一遍,所以不同对象中都会有自己的一批锁,线程访问不同对象中带锁的代码,只会获取独属于对象的那一批锁。所以访问同一个类的不同实例化带锁的代码时,他们并不会发生争抢锁的情况。
2.获取类锁:类锁不同于对象锁,类锁是该类所有实例化对象共用一批类锁,只要访问该类的静态带锁代码,就是去申请同一批锁
注*一个对象可能有一批对象锁,还有一批类锁
锁对象时:
当线程实现的是Runnable接口时:
注意这里使用了同步代码块,缩小锁的范围,如果将循环也锁进去,一个线程会一直占用pay()方法,直到票买完,因为线程执行完锁内的代码才会释放锁,一直循环就一直执行不完,所以不释放。
如果非要把循环锁进去,可以将Thread.sleep(1000)替换成this.wait(1000),因为wait()方法会释放锁,这样其他线程就有机会拿到锁了
class My_Thread implements Runnable{
private int count=100;
@Override
public void run() { pay(); }
public void pay(){
while(true) {
synchronized(this){
if(count<=0){break;}
System.out.println("窗口"+Thread.currentThread().getName()+"余票"+--count);
try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
}
案例1:三个线程其实执行的是一个对象,三个线程竞争同一批锁,共卖100张票
My_Thread mythread=new My_Thread();//带锁的对象
Thread thread1=new Thread(mythread);
Thread thread2=new Thread(mythread);
Thread thread3=new Thread(mythread);
thread1.start();
thread1.start();
thread3.start();
/*
结果:
窗口Thread-0余票3
窗口Thread-2余票2
窗口Thread-1余票1
窗口Thread-2余票0
*/
案例2:两个线程执行的是不同的对象,所以竞争的不是同一批锁,各卖100张票,共200张
My_Thread mythread=new My_Thread();
My_Thread mythread2=new My_Thread();
Thread thread1=new Thread(mythread);
Thread thread2=new Thread(mythread2);
thread1.start();
thread2.start();
/*
结果:
窗口Thread-2余票1
窗口Thread-0余票1
窗口Thread-0余票0
窗口Thread-2余票0
*/
上面说只有多个线程访问同一对象同一标识(参数相同的锁)的锁时才会竞争资源,所以案例一会竞争同一批锁,他们会争抢使用mythread对象的pay()方法的权力;
案例2不会竞争同一批锁,thread1线程使用的是mythread的pay()方法,改变的也是mythread的count值,而thread2线程使用的是mythread2的pay()方法,改变的是mythread2的count值。
这就是对象锁的用法,并不是线程使用该代码就会竞争锁,必须是访问的同一对象下的改代码才会出现竞争行为。
当线程继承Thread时:
class My_Thread extends Thread{
private int count=100;
@Override
public void run() { pay(); }
public void pay(){
while(true) {
synchronized(this){
if(count<=0){break;}
System.out.println("窗口"+Thread.currentThread().getName()+"余票"+--count);
try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
案例:创建两个线程对象并开启,此时两个线程个卖出100张,共卖200张票
My_Thread mythread=new My_Thread();
My_Thread mythread2=new My_Thread();
mythread.start();
mythread2.start();
因为两个线程访问的不是同一对象的锁所以没有竞争关系
锁类:
这里要补充一点之前的知识点:就是访问普通方法,会在堆中找到对象,对象里面有指向虚方法表的引用,然后通过通过方法区中的虚方法表拿到方法区类加载后的二进制元数据,去执行方法。
如果是访问类的静态方法,会通过编译时期生成常量池去获取静态方法在方法区的元数据地址,然后执行。
在静态方法上加synchronized就是类锁(Class映射)
案例:
public class test {
public static void main(String[] args) throws InterruptedException {
My_Thread mythread=new My_Thread();
My_Thread mythread2=new My_Thread();
mythread.start();
mythread2.start();
}
}
class My_Thread extends Thread{
private static int count=100;
@Override
public void run() {
while(true){ pay(); }
}
public static synchronized void pay(){
if(count<=0){return;}
System.out.println("窗口"+Thread.currentThread().getName()+"余票"+--count);
try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
}
虽然开启的两个线程执行的是不同的对象,但是执行的是同一个类的静态方法,都要经Class映射,所以两个线程竞争的同一批类锁。
在静态方法中使用同步代码块,也是类锁:
public static void pay(){
synchronized(My_Thread.class){
if(count<=0){return;}
System.out.println("窗口"+Thread.currentThread().getName()+"余票"+--count);
try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}
}
}
效果同上!
=========================================================================同步代码块:
1.用在普通方法中
synchronized( this ){}
2.用在静态方法中
synchronized( 类名.class ){}
因为一个对锁象,一个类锁,所以参数不一样。
=========================================================================
释放锁:
1.执行完带锁的代码就会自动释放
2.在带锁代码中遇见return;
3.线程对象方法.wait()方法,会释放锁
3.程序出现异常未处理而终止程序,同时也会释放锁
不释放锁:sleep() yield() join()都不会释放锁,只有wait()会释放锁