在了解多线程之前,先来了解一下进程与线程之间的关系。
进程和线程:
进程是指在系统中正在执行的一个程序,每个进程之间是独立的。
线程是进程的一个基本执行单元。一个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)
主线程:
在java程序中存在一个主线程(JVM线程),main方法自带的一个线程。
之所以在系统进行多个程序的时候(QQ,web网页等),看似是在同步执行,那是因为cpu在进程中进行多线程的切换,cpu切换的速度之快让我们 觉得是在同步执行,其实在进程的执行过程中是存在一定的先后顺序的。
线程的执行原理:
线程运行的结果每次都不同,因为多个线程都在获取cpu的执行权,cpu执行到了谁,谁就执行。
明确一点:在某一时刻,只能有一个程序在运行(多核cpu除外)——也可以说多线程的运行行为是在互相抢夺cpu的执行权,同时cpu在做着快速的切换动作,已达到看似同时运行的结果。因此多线程的特性是随机性。
实现多线程的方式有两种:继承Thread类、实现Runnable接口
继承Thread类
1、定义类继承Thread类
2、复写Thread中的run()方法
目的:将自定义代码写在run()方法中,让线程运行
3、调用start()方法,启动线程,调用run()方法
(一个线程是不允许调用两次start()方法,第二次调用时会抛出java.lang.IllegalThreadStateException异常。多次调用会认为是变成错误。
在第二次调用 start() 方法的时候,线程可能处于终止或其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。)
public class ThreadTest extends Thread{ @Override public void run(){ //复写Thread中的run方法 for(int i=0;i<10;i++){ System.out.print(" demo run"); } } } class ThreadDemo{ public static void main(String[] args) { ThreadTest dt = new ThreadTest(); dt.start(); //调用start方法(启动线程,调用run方法) for(int i=0;i<10;i++){ System.out.print("main run"); } } }
代码解说:在这一个程序中,存在两个线程:main主线程和ThreadTest自定义线程。
这两个线程同是在一个进程(程序)中执行,为确保程序的正常执行,因此在进程的内存空间中,开辟了两个线程空间,这两个线程空间在要执行的时候,都要获取cpu的执行权,所以,会互相的去抢夺cpu的执行权。会先抢到谁就执行。因此,这段程序的运行结果是随机的。
意思就是说:运行结果一会是main run,一会是demo run。存在无序性。
至于为什么要复写run方法,原因是:
Thread用于描述线程,该线程就只定义了一个功能,用于存储线程要运行的代码,该存储功能就是run()方法。也就是说Thread中的run()方法,用于存储线程要允许的代码。
(主线程的代码写在main方法中,而main方法是JVM定义的。所以JVM调用main方法的原因是因为主线程要使用,如果将自定义的代码写在main方法中,让JVM去执行,那就是一种单线程(主线程),不是多线程了。)
public class ThreadTest extends Thread{ @Override public void run(){ for(int i=0;i<10;i++){ System.out.print(" demo run"); } } } class ThreadDemo{ public static void main(String[] args) { ThreadTest dt = new ThreadTest(); dt.start(); //① dt.run(); //② for(int i=0;i<10;i++){ System.out.print("main run"); } } }
代码解说:在上段程序中,如果①被注释掉,直接dt.run(),那这就不是一个多线程的执行,只是相当于一个简单的对象调用方法,而线程创建了,并没有运行。
所以,如果②被注释掉,直接dt.start(),那就是创建了一个线程对象dt,然后dt.start()将线程dt开启,并调用了run()方法,让线程去运行。
实现Runnable接口
1、定义类实现Runnable接口
2、复写Runnable接口中的run()方法
3、通过Runnable建立线程对象
4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
目的:因为自定义的run()方法,所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run()方法,就必须明确该run()方法的所属对象
5、调用Thread类的static方法开启线程并调用run()方法
class RunnableTestDemo implements Runnable{ @Override public void run() { System.out.println("实现Runnable接口"); } } public class RunnableTest { public static void main(String[] args) { RunnableTestDemo r = new RunnableTestDemo(); Thread t1 = new Thread(r); t1.start(); } }
实现Runnable接口与继承Thread的区别:
1、接口方法避免了Java单继承的局限性
2、继承Thread,线程代码存放在Thread子类run()方法中
3、实现Runnable接口,线程代码存放在接口紫子类的 run()方法中
所以,在选择实现Runnable接口与继承Thread这两者中,建议使用实现Runnable接口的方式
线程的状态
见上图,可看到线程的状态分为:
1、创建线程(new 线程类),调用start()方法执行run()方法
2、运行状态:具有执行资格,并且有执行权
3、冻结状态:没有执行资格,没有执行权
4、临时状态(阻塞):具有执行资格,没有执行权。等待cpu的执行权
5、消亡状态:线程结束
根据线程的状态的执行原理是:创建一个线程A,调用start()方法,执行run()方法,当线程运行时,遇到sleep(time)或者wait()方法,线程就会进去冻结状态(放弃了执行资格),只有当sleep(time)或者执行了notify()方法,冻结状态就会变成临时状态(阻塞),这时临时状态的线程就会去同其他的线程一起抢夺cpu的执行权,一旦抢到cpu的执行权,该临时状态的线程就会运行(run),变成运行状态run,直到run()方法中的代码执行完,线程才会结束,变成最后的消亡状态。
同步代码块
较多情况下,Java在处理多线程的时候,会出现一些安全问题,而Java处理多线的安全问题的是方式,是使用同步代码块(synchronized(对象/this/类名.class))或者同步代码函数。
但是在解决多线程的安全问题时,使用同步代码块的前提是:
1、必须要有两个或者两个以上的线程
2、必须是多个线程使用同一个锁(如果存在多个类使用到同步,那么在这几个类中找唯一类,也就是找使用到同步快的共同类)
3、必须保证听你同步代码块中只能有一个线程在运行。
同步代码块的好处是解决了线程的安全问题,坏处是多个线程进去同步代码块的时候都要去判断,较为消耗资源。
在使用同步代码块的前提下,得确认线程是否存在安全性问题,如果找到线程的安全性问题;
1、明确哪些代码是多线程运行的代码
2、明确共享数据
3、明确多线程运行代码中哪些语句是操作的共享数据
同步代码块或者同步代码函数的运行原理:
1、将需要解决安全问题的代码放到同步代码块中
2、当多个线程运行时,第一个线程A获得cpu的执行权,首先会来判断真假是否持有锁,如果有,线程A就会进入到同步代码块中,并且进入到里面的第一件时间就是将同步锁给关闭。
3、然后线程A在同步代码块中,执行同步代码块中的代码。
4、在此之前,其他线程即使获得cpu的执行权也无法进入到同步代码块中,因为没有获得锁。
5、当线程A将同步代码块中的代码执行完毕时,最后一步是打开同步锁,然后退出。
6、这时,其他线程就可以获得cpu的执行权后并且获得同步锁,进入到同步代码块中,执行操作。
public class MyThread implements Runnable{ private static int i = 100; //多个线程共享的数据 boolean flag = true; public void printVal(){ if(flag){ while(true){ synchronized(this){ if(i>0){ System.out.println("-----code"+i--); } } } }else{ while(true){ printStaticVal(); } } } public static void printStaticVal(){ while(true){ synchronized(MyThread.class){ if(i>0){ System.out.println("-----Static"+i--); } } } } public void run(){ printVal(); printStaticVal(); } } class TestSync { public static void main(String[] args) { TestSync t = new TestSync(); Thread my1 = new Thread(t); Thread my2 = new Thread(t); my1.start(); my2.start(); } }
上述代码解析的结果是:有两个线程my1和my2,当my1首先抢到cpu的执行权,进去到run方法中,run方法中调用了printVal(val)方法。在这个方法上使用到了同步(synchronized),这样就相当于一个同步函数。当my1进来访问时,首先进行判断是否有锁,如果有,就会进去printVal(val)方法,然后首先就将锁给关闭,然后执行for循环,打印v的值。当for循环结束时,线程my1就会开启同步锁,然后出去。这时线程my2可能就会抢到cpu的执行权,然后进入到同步函数中。
如果printVal(val)方法不是同步方法,那么就会出现当线程my1抢到执行权进入run方法,执行for循环,当i=1时,执行到if(i>0)完,cpu的执行权被线程my2抢到了,这时它也来判断i = 1,i>0,满足,判断完后,这时cpu的执行权又被my1抢到了,这时,i被打印,然后i--,这时线程my2抢到了,它就不会去执行if判断了,它答应出来i的值就变了-1,这时就出现了多线程的安全问题。
出现多线程的原因是:当多条语句在操作同一个线程共享数据时,一个线程对多余语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。如果一个让一个线程进入方法将语句全部执行完再让其他的线程进来执行,那么线程的安全性的问题就会被解决。因此,我们在方法体上添加了同步(synchronized)。
在上述代码中,会发现同步代码块(synchronized)后面带的参数(锁)有所不同,函数需要被对象调用,那么函数都有一个对象引用this,所以,一般方法里,同步使用的锁是this。但是如果同步函数是个静态的,而静态方法中不可以定义this,静态在进内存时,内存中没有本类的对象,但是一定有该类对应的字节码对象(类名.Class)——该对象的类型是Class。因此静态同步方法使用的锁是该同步方法所在类的字节码对象(类名.Class)。
在Java中,还有一种机制:等待-唤醒机制(wait-notify,notifyAll)
线程在运行时,线程会开启一个线程池,等待线程就存在线程池中,而且唤醒的是唤醒线程池中第一个等待的线程。
这种机制用于同步中,因此这种机制在操作同步中的线程时,都必须要标识它们说操作线程持有的锁,只有同一个锁上被等待的线程,才能被痛一个锁给唤醒(notify),不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在object中。
停止线程(run方法结束)
开启多线程运行,运行代码通常是循环结构的,只要控制循环就可以让run方法结束,也就是线程结束。