Skip to content

Latest commit

 

History

History
234 lines (217 loc) · 8.41 KB

11-多线程.md

File metadata and controls

234 lines (217 loc) · 8.41 KB

#8-11

程序、进程和线程

程序

程序是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。 (程序是静态的)

进程

是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程

线程

进程可进一步细化为线程, 是一个程序内部的一条执行路径。 若一个进程同一时间并行执行多个线程,就是支持多线程的。


并行和并发

并行

多个CPU同时执行多个任务

并发

一个CPU同时执行多个任务


创建线程的三种方式

1.继承Thread类

继承Thread类的类需要重写run方法 在需要开启新线程的时候,调用对象的start()方法

/**  
 * 线程子类  
 * 拥有抢夺资源的能力 : 继承Thread  重写其中run方法  
 */  
class NewThread extends Thread{  
    @Override  
    public void run() {  
        for (int i = 0; i < 20; i++) {  
            System.out.println("子线程,启动:"+i);  
        }  
    }  
}
class main {  
    public static void main(String[] args) {  
        for (int i = 0; i < 10; i++) {  
            System.out.println("主线程,启动:"+i);  
        }  
        //创建一个线程类  
        new NewThread().start();  //如果调用run, 相当于调用普通方法 不是开启一个新的线程  
        for (int i = 0; i < 10; i++) {  
            System.out.println("主线程二,启动:"+i);  
        }  
    }  
}

线程默认的名字是 Thread-[数字] 主方法的线程名是 main 通过调用线程类的setName()方法可以自定义线程名,也可以通过构造函数来自定义线程名

2.实现Runnable接口

实现Runnable接口,必须实现其中的run()方法

/**  
 * 创建线程2: 实现Runnable接口  
 */  
class Runable implements Runnable{  
    @Override  
    public void run() {  
        for (int i = 0; i <= 100; i++) {  
            System.out.println(Thread.currentThread().getName()+"正在运行: "+i);  
        }  
    }  
}
class Test {  
    public static void main(String[] args) {  
        Runable runable = new Runable();  
        new Thread(runable, "子线程").start();  
        //注意实现Runnable接口的线程启动方式
        
        for (int i = 0; i < 100; i++) {  
            System.out.println(Thread.currentThread().getName()+"正在运行:"+i);  
        }  
    }  
}

Thread和Runnable是实现java多线程的2种方式,Runable是接口,Thread是类,建议使用Runable实现 java多线程,不管如何,最终都需要通过thread.start()来使线程处于可运行状态。

3.实现Callable接口

Callable接口引入

对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式,都需要有一个run方法, 但是这个run方法有不足: (1)没有返回值 (2)不能抛出异常 基于上面的两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口。 实现Callable接口好处: (1)有返回值
(2)能抛出异常 缺点: 线程创建比较麻烦

class CallableTest implements Callable {  
    @Override  
    public Object call() throws Exception {  
        int i = 0;  
        for ( i = 0; i < 10; i++) {  
            System.out.println(Thread.currentThread().getName()+"线程运行中:"+i);  
        }  
        return i;  //自动装箱  
    }  
}  
class Test{  
    public static void main(String[] args) throws ExecutionException, InterruptedException {  
        FutureTask futureTask = new FutureTask(new CallableTest());  
        Thread thread = new Thread(futureTask,"仔仔");  
        thread.start();     //开启callable线程
  
        for (int i = 0; i < 10; i++) {  
            System.out.println(Thread.currentThread().getName()+"线程运行中:"+i);  
        }  
        Object o = futureTask.get();  
        System.out.println(o);  
    }  
}

三种实现方式的区别:

继承Thread类:

  • 创建线程最简单的方式之一,只需要继承Thread类并重写run方法。
  • 优点:代码比较简单,适合简单的线程任务。
  • 缺点:由于Java不支持多重继承,如果已经继承了其他类,就无法使用这种方式创建线程。

实现Runnable接口:

  • 创建线程的推荐方式,可以避免单继承限制,适合多线程共享同一个资源的情况。
  • 需要实现run方法,并将实现了Runnable接口的对象作为参数传递给Thread类的构造函数。

使用Callable和Future接口:

  • 这种方式允许线程返回结果,相对于前两种方式,更加灵活。
  • Callable是一个带有返回值的任务,而Runnable只是一个执行的任务。
  • 使用Executor框架调度Callable任务,并通过Future对象来获取线程的执行结果。

线程的生命周期

Thread类中有一个内部类Thread.State,其中定义了一些枚举常量,他们代表了线程生命周期中的几个状态

  1. NEW 新建状态,线程刚刚被创建,尚未调用 start() 方法。
  2. RUNNABLE 就绪状态,线程已经被创建且可以被调度执行,但尚未获得CPU资源。
  3. BLOCKED 阻塞状态,线程被阻塞,正在等待获取一个监视器锁
  4. WAITING 等待状态,线程进入等待状态,等待另一个线程通知或中断。
  5. TIMED_WAITING 限时等待状态,线程等待一个具有超时时间的操作。
  6. TERMINATED 终止状态,线程已经执行完毕。

线程的常用方法

  1. start() 启动当前线程
  2. run() 线程执行的内容/单独调用时视为普通方法
  3. currentThread() 静态方法:获取当前正在执行的线程
  4. setName() 设置线程名
  5. getName() 获取线程名
  6. currentThread().setPriority() 设置当前线程优先级
  7. currentThread().getPriority() 获取当前线程优先级
  8. join() 当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。
  9. sleep(long millis) 人为的制造阻塞事件
  10. setDaemon() 设置伴随线程

线程安全

同步代码块

synchronized ( 🔒 ){  
	...
}
🔒 :多个线程必须共用一把锁 (同步监视器)。锁必须是引用数据类型。
	同步代码块中不能改变锁对象的引用
	尽量不要使用String和Integer做锁
	使用final修饰锁

同步方法

public class BuyTicketThread extends Thread {
    public BuyTicketThread(String name){
        super(name);
    }
    //一共10张票:
    static int ticketNum = 10;//多个对象共享10张票
    //每个窗口都是一个线程对象:每个对象执行的代码放入run方法中
    @Override
    public void run() {
        //每个窗口后面有100个人在抢票:
        for (int i = 1; i <= 100 ; i++) {
            buyTicket();
        }
    }
    public static synchronized void buyTicket(){// 锁 BuyTicketThread.class
        if(ticketNum > 0){//对票数进行判断,票数大于零我们才抢票
            System.out.println("我在"+Thread.currentThread().getName()+"买到了从北京到哈尔滨的第" + ticketNum-- + "张车票");
        }
    }
}

Lock锁

public class BuyTicketThread implements Runnable {
    int ticketNum = 10;
    //拿来一把锁:  (可住入锁)
    Lock lock = new ReentrantLock();//可重入锁 多态 可以使用不同的实现类
    @Override
    public void run() {
        //此处有1000行代码
        for (int i = 1; i <= 100 ; i++) {
            //上锁
            lock.lock();
            try{
                if(ticketNum > 0){
                    System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第" + ticketNum-- + "张车票");
                }
            }catch (Exception ex){
                ex.printStackTrace();
            }finally {
                //开锁:--->即使有异常,这个锁也可以得到释放
                lock.unlock();
            }
        }
        //此处有1000行代码
    }
}

使用线程同步的优缺点

优点:安全 缺点:效率低 容易造成死锁。解决方法:减少同步资源的定义,避免同步代码的嵌套。