引言
我们今天所使用的操作系统是多任务操作系统。多线程就是是实现多任务的一种方式。一个程序可以看作为一个进程,进程是是一个正在执行中的程序,每一个进程执行都是有一个执行顺序,该顺序是一个执行路径或者叫一个控制单元。用于封装每一个程序的控制单元。比如在Windows系统中,一个运行的exe就是一个进程。而线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。
一个进程中至少有一个线程。比如深究JVM,JVM启动都不是单线程,因为除了主线程之外还有负责垃圾回收GC的一个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。总的来说,CPU真正执行是线程,在CPU某一时刻永远都是在执行一个程序,即永远只有一个控制单元在执行(多核除外),实际情况cpu总是在极短的时间内不停地切换执行路径即线程,是因为切换时间极短所以根本感受不到。
一、创建线程的方式
1.继承Thread类,然后重写run方法
- 继承Thread,重写run的方法,(因为父类Thread的run方法中并没有任何操作代码,而我们需要执行自己的功能就应该复写父类的run)
- 创建线程对象,调用线程的start方法启动线程
2.实现Runnable接口,然后实现其run方法。
- 实现Runnable 的接口,实现run方法
通过Thread类的构造方法Thread(Runable r)创建Thread对象(为什么要将Runnable借口的子类对象传递给Thread的构造函数,因为自定义的run方法所属对象是Runnable接口的子类对象,要想让线程去执行指定对象的run方法,就必须明确该run方法的所属对象)
二多线程的简单应用
例子:模拟火车站卖票窗口卖票900,多个窗口同时卖票
2.1继承Thread方式
class SaleTicket extends Thread{
private staic int num=900;//因为总的票数是所有对象共享的,但是并不是优秀方案,宜采用Runnable的方式实现多线程
SaleTicket(String name){
super(name);
}
/**
因为虚拟机定义时,Thread类的run方法就是用于存储线程要运行的代码,如果不重写run方法,虚拟机加载run方法的时候,找不到执行代码。
*/
@override
pubic void run(){
while(true){
if(num>0){
System.out.println(Thread.currebtThread().getName()+"Sale :"+num--");
}
}
}
}
测试类:
//因为虚拟机定义时,而主线程的运行代码就是存在main方法里的
public class TestThread {
public static void main(String[] args) {
SaleTicket d=new Demo("1窗口");//创建好了一个线程,即CPU中的一个控制单元
SaleTicket d2=new Demo("2窗口");
SaleTicket d2=new Demo("3窗口");
d.start();//开启线程并执行该线程的run方法
d2.start();
d3.start();
//d.run();仅仅是在主线程调用了run方法,线程创建了,但并没有运行,还是单线程程序,只有在线程的run方法执行了之后才会执行下面的循环语句。
for(int x=0;x<60;x++){
System.out.println("Main 线程 run");
}
}
执行情况:是main线程和SaleTicket 线程交替执行的,因为在CPU某一时刻永远都是在执行一个程序,即永远只有一个控制单元在执行(多核除外),实际情况cpu总是在极短的时间内不停地切换执行路径即线程,是因为切换时间极短所以根本感受不到。多线程执行的时间由cpu说的算。
执行情况:主线程 做两个部分的工作,一创建多线程d,d2,d3,创建了线程之后,启动d,d2,d3,二执行循环语句,而线程d,d2,d3只负责run方法内部分工作。
2.2实现Runnable接口并实现run方法(推荐)
class ThreadRunableDemo implements Runnable{
private int num=1000;
Object obj=new Object();
/**
因为虚拟机定义时,Thread类的run方法就是用于存储线程要运行的代码
*/
@override
pubic void run(){
while(true){
/**
如果不加上,同步代码块synchronized可能会出现问题:
会卖出0,-1,-2等错票。
原因:当多条语句在操作同一线程共享数据时,一个线程中多条语句执行了一部分,还没有执行完的时候,另一个线程参与进来执行,导致共享数据的错误。
解决方案:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即即使拿到了执行权也不能让他执行。 即利用同步代码块把操作共享数据的语句同步起来。
*/
synchronized(obj){
if(num>0){
try{
Thread.sleep(20);
}
catch(Exception e){
}
System.out.println(Thread.currebtThread().getName()+"Sale :"+num--");
}
}
}
}
测试:
//因为虚拟机定义时,而主线程的运行代码就是存在main方法里的
public class TestRunnable {
public static void main(String[] args) {
//Runnale 的接口实现方式
ThreadRunableDemo tr=new ThreadRunableDemo();
Thread tr1=new Thread(tr);
Thread tr2=new Thread(tr);
Thread tr3=new Thread(tr);
tr1.start();
tr2.start();
tr3.start();
for(int x=0;x<60;x++){
System.out.println("Main 线程 run");
}
}
如果不加上同步代码块的话,可能会导致出现安全问题。重点内容现在开辟了3个线程,都在操作num,因为线程都是随机性的,是由CPU决定的,假设cpu执行1线程,1线程进入到CPU,判断num是否大于0,刚刚判断完,1线程刚刚准备执行卖票,具备执行资格,但是cpu切换到了其他线程2,2线程获得了执行资格,但是也有可能是被切换了到了3,3也具备执行资格,而1、2、3 都挂着的时候,CPU再切回了1,执行了卖票,num变成0的时候,此时3获得执行权,就不需要再判断了直接执行卖票,但是票已经没有了,再执行到3线程的时候num已经变成了-1号票,已经出现了安全问题,因为在执行线程的时候,cpu有可能切换到其他的的线程。
三、多线程并发
比如以上例子的,开辟了三个窗口(线程)同时售票,多线程并发是线程同步中比较常见的现象,java多线程为了避免多线程并发解决多线程共享数据同步问题提供了synchronized关键字
1. synchronized关键字:当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
- 对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。 Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法再去访问该synchronized方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),那么将该对象的锁释放掉,其他线程才有可能再去访问该synchronized方法。
- 如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。
- 如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,他们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行。
- synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的
- 同步代码块:表示线程在执行的时候会对object对象上锁。
//对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
synchronized(对象){
//需要同步的代码块
}
2. 同步代码块synchronized的通俗形象描述:
synchronized就像是多线程一个”看门狗”,当初始化的时候,cpu中没有对应的线程的时候,此时任何线程都可以进入到cpu获取执行权然后运行相关代码。拿买票的说,一开始主线程创建了3个线程1 、2、3,调用start方法之后,假设1线程首先被执行,1得先进来,进来之后,做的第一件事应该是改变”看门狗”的状态(可以理解为标记位,标记位有人),改变了状态之后再去执行if判断,满足则有可能执行到sleep然后释放了执行权,此时2 、3 都有可能争抢执行权,假设2争取到了,2想进入到同步代码块,也会先经过同步代码块,这“看门狗”的标记位还是”有人“,2即使是撞得头破血流”看门狗“也不会放他进来,此时1睡醒了,继续执行接下的代码,卖票,出了同步代码块,把”看门狗“标记位置为”无人“,释放了执行权,此时假设3进来,就又可以执行了,如此反复。
3. 同步的前提
- 必须要有两个或者以上的线程
- 必须是多个线程使用同一个锁
- 必须保证同步中只有一个线程在运行
4. 同步代码块虽然能解决多线程的安全性问题,但是消耗资源,所以不是所有的代码都应该放到同步代码块中,只有都符合以下条件才应该用同步处理:
- 只有运行在多线程的代码
- 共享数据
- 操作共享数据的语句
四、关闭线程的方法
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果,一般要设定一个变量,在run方法中是一个循环,循环每次检查该变量,如果满足条件则继续执行,否则跳出循环,线程结束。)。
- 使用interrupt方法来终端线程可分为两种情况:
(1)线程处于阻塞状态,如使用了sleep方法。
(2)使用while(!isInterrupted()){……}来判断线程是否被中断。
五、继承Thread方式和实现Runnable接口方式的区别
两种方法均需执行线程的start方法为线程分配必须的系统资源、调度线程运行并执行线程的run方法。实现方式避免了单继承的局限性,在定义线程时宜采用。两者相比,线程执行的代码存放在Thread子类的run方法,而实现Runnable时线程数放到实现接口的子类。