多线程的基本使用

本文详细介绍了Java中的多线程概念,包括程序、进程和线程的区别,线程的创建(继承Thread、实现Runnable、Callable接口和使用线程池)以及线程的生命周期。此外,还探讨了线程同步的三种方式(同步代码块、同步方法和使用锁)以及线程通信、死锁问题和守护线程的应用。

一、概念

1.程序、进程、线程

程序

计算机程序是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。程序是一个指令序列。

进程

是系统进行资源分配和调度的基本单位,是操作系统结构的基础。进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。 

线程

操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

一个进程中有多个线程,没条线程做不同的事情,并且共享其中的资源;

2.并发与并行

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机运行,但任一个时刻点上只有一个程序在处理机上运行。(窗口抢票)

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

二、线程的创建和使用

方式一:继承Thread类创建多线程

1.基本使用

步骤:

  1. 继承Thread类 
  2. 重写Thread类的run方法---->将此线程执行的操作放到run方法中 
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start()方法 
public class ThreadDome01 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }

    public static void main(String[] args) {
        ThreadDome01 t1 = new ThreadDome01();
        //1.导致此线程开始执行; 2.Java虚拟机调用此线程的run方法。
        t1.start();
        //问题1:我们不能直接调用run()方法启动线程
        //问题2:不能让已经start()的线程再次调用start方法

        //这个操作通用是在main线程中执行
        for (int i = 0; i < 100; i++) {
            if(i%2==0){
                System.out.println("主:"+i);
            }
        }

    }
}

 2.Thread常用的方法

  1. strat():启动当前线程,调用当前线程的run();

  2. run():通常需要重写Thread类中的此方法,将创建的线程要执行的的操作声明在此方法中;

  3. currentThread():静态方法,返回当前线程;

  4. getName():获取当前线程的名字;

  5. setName():设置当前线程的名字;

  6. yield():线程礼让(释放当前CPU的执行权);

  7. join():插队(线程A阻塞,执行线程B,直到B线程执行完成);

  8. stop():强制结束(过时了);

  9. sleep(long millitime):休眠,让线程暂时停一会;

3.优先级的调度

MIN_PRIORITY = 1;

NORM_PRIORITY = 5; 默认;

MAX_PRIORITY = 10;

2.获取和设置 getPriority() setPriority();

优先级高只是说抢占CPU的执行权可能性要高,而不是绝对的高

方式二:实现Runnable接口创建多线程

1.基本使用

步骤:

  1. 创建一个实现了Runnable接口的类;
  2. 实现类去实现Runnable中的抽象方法 run();
  3. 创建实现类的对象;
  4. 将此对象作为参数传递到类的构造器中,创建Thread类的对象;
  5. 通过Thread类的对象调用start();
class MyThead implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class CreateRunable {
    public static void main(String[] args) {
        MyThead m  = new MyThead();
        //通过Thread类的对象调用start(),启动线程
        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);
        t1.setName("线程一");
        t2.setName("线程二");
        t1.start();
        t2.start();
    }
}

两种方式的对比

class WindowsFall implements Runnable{
    private  int ticket = 100;
    @Override
    public void run() {
        while (true){
            if (ticket>0){
                System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
                ticket--;
            }else {
                break;
            }
        }
    }
}

public class exeWindowTest2 {
    public static void main(String[] args) {
        WindowsFall w = new WindowsFall();
        Thread t1 = new Thread(w);
        t1.setName("窗口1:");
        Thread t2 = new Thread(w);
        t2.setName("窗口2:");
        Thread t3 = new Thread(w);
        t3.setName("窗口3:");
        t1.start();;
        t2.start();
        t3.start();
    }
}

 实现Runnbale接口来写的卖票例子,如果使用的是继承Thread方式来写的话就必须是private static int ticket = 100;

问题一:为什么继承Thread类的多线程要加static?

1.使用继承Thread类的方式实现多线程,在使用时肯定是要实例化多个Thread类的,而且执行代码写了在Thread类的run方法中,所以就相当于多个线程的是单独的,所以我要加static关键字才能使其资源共享;

2.使用实现Runnable接口的方式实现多线程,只需要实例化一个Thread类就可以,而执行代码是在Runnable的run方法中;我在使用时,是把多个继承Runnable方法的run()都塞到一个Thread实例中,所以资源共享;

问题二:为什么把实现Runnable接口的实现类传入Thread(),就能通过start()方法调用?

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    private Runnable target;


    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

上面是Thread类的部分源码,意思是如果有实现Runnable接口的类且实例化了对象,那么target就不是空的,就会执行target.run()方法,而 target.run()方法就是我们实现Runnable接口的类重写的run方法;

方式三:实现Callable接口

步骤:

  1. 创建一个实现Callable的实现类,需要执行的操作放在类中
  2. 创建实现了Callable接口的实现类对象,
  3. 实例化FutureTask类的对象,把 2 的对象作为参数给 3
  4. 把 3 作为参数传递给 new Thread()
  5. 调用start()
  6. 可通过get()方法获取第二步中返回值
public class testcall implements Callable<Boolean> {
    private String url; //网络图片的地址
    private String name;    //不好村的文件名
    public testcall(String url,String name){
        this.url=url;
        this.name=name;
    }

    @Override
    public Boolean call() throws IOException {
        //实现方法
        WebDownload webDownload = new WebDownload();
        webDownload.DownLoad(url,name);
        System.out.println("下载了名为:"+name+"的文件");
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        testcall ti = new testcall("url","1.png");
        testcall t2 = new testcall("url","2.png");
        testcall t3 = new testcall("url","3.png");

        //创建执行服务
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Boolean> ri = executorService.submit(ti);
        Future<Boolean> r2 = executorService.submit(ti);
        Future<Boolean> r3 = executorService.submit(ti);
        //获取结果
        boolean res1 = ri.get();
        boolean res2 = ri.get();
        boolean res3 = ri.get();
        //关闭服务
        executorService.shutdown();
    }
}


//下载图片的类与方法
class WebDownload{
    public void DownLoad(String url,String name) throws IOException {
        FileUtils.copyURLToFile(new URL(url),new File(name));
    }
class NumThread implements Callable{
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i%2==0){
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();
        FutureTask futureTask = new FutureTask(numThread);
        new Thread(futureTask).start();
        try {
            //get()返回值即为futureTask构造器参数Callable实现类重写的call()返回值
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

这个方式可以的好处是:1.可以提供返回值;2.支持泛型;3.可以抛出异常从而被外面的类所捕获;

方式四:使用线程池的方式

步骤:

  1. 提供指定线程数的线程池
  2. (可以提供一下特定的设置)
  3. 执行指定的线程操作
  4. 关闭线程池
class NmThread implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <=100 ; i++) {
            System.out.println(i);
        }
    }
}

public class MyThreadPool {
    public static void main(String[] args) {
        //1.提供指定线程数的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //设置属性
        ThreadPoolExecutor executorService1 = (ThreadPoolExecutor) executorService;

        //2.执行指定的线程操作
        executorService.execute(new NmThread()); //适合使用Runnable
        //executorService.submit(); //适合使用Callable
        //3.关闭线程池
        executorService.shutdown();
    }
}

好处:

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低了资源的消耗,(重复利用线程池中的线程,不需要每次都创建,便于集中管理)

 常用的参数配置:

Executors工具类,线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n)创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n)创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
  1. corePoolSize:核心线程数
  2. queueCapacity:任务队列容量(阻塞队列)
  3. maxPoolSize:最大线程数
  4. keepAliveTime:线程空闲时间
  5. allowCoreThreadTimeout:允许核心线程超时
  6. rejectedExecutionHandler:任务拒绝处理器

三、线程的生命周期

五种状态:

  • 新建:一个Thread类或者子类被实例化时,就会处于新建状态;
  • 就绪:当被调用了start()方法后并不是直接就会运行,而是处于就绪状态,等待CPU分配;
  • 运行:获得CPU分配的资源后开始运行;
  • 阻塞:在一些特殊情况下,线程会让出自己的执行权,停止自己的运行状态从而进入阻塞状态;
  • 死亡:完成工作或者调用了stop()等方法;

四、线程的同步

在多个线程操作共同资源的时候会发生线程的安全问题,出现的原因是,当A线程在操作一个资源的时候,B线程随后也来操作了这个资源,这样的话就会导致线程安全问题;在实际情况中应该是看到有人在使用一个资源,那我就去使用别的资源,但是程序是死的,所以就会有这样的问题;那么解决的办法就是使用线程同步;

三种线程同步方式

1.同步代码块

 private static int ticket=100;
    private static Object obj = new String();
    @Override
    public void run() {
        while (true){
            synchronized (obj){
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName() + "卖了第:" + ticket + "张票");
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }

在有操作共同资源的代码段上用 synchronized 包裹起来, 里面在参数是同步监视器,且多个线程只能共用一个,也就是锁;

同步监视器:锁(获得执行权的线程获得锁,任何一个对象都可以充当锁,当时要求多个线程要共用一把锁)

相当于武林盟主的令牌,谁有这个令牌谁就有执行的权力;

2.同步方法

class WindowsFall implements Runnable{
    private  int ticket = 100;
    @Override
    public void run() {
        while (ticket>0){
            maipiao();
        }
    }

    private synchronized void  mp(){
        if (ticket>0){
            System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
            ticket--;
        }
    }
}

在有操作共同资源的代码段中抽离出来作为一个方法,同时加上 synchronized 关键字; 

 3.使用锁

class Windows implements Runnable{
    private int ticket =100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            //调用lock方法
            lock.lock();
           try{
               if(ticket>0){
                   System.out.println(Thread.currentThread().getName() + "卖了第:" + ticket + "张票");
                   ticket--;
               }else {
                   break;
               }
           }finally {
               //3.解锁
               lock.unlock();
           }
        }
    }
}

总结:

synchronized Lcok 都是用来解决线程安全问题的方式;

synchronized机制在执行完相应的同步代码后,自动的释放同步监视器 lock需要手动的启动同步lock(),同时结束时也需要手动的实现unlock()

使用顺序:lock   同步代码块   同步方法

五、线程的通信

线程通信

基本使用

    @Override
    public void run() {
        while (true){
            synchronized (this){
                //唤醒所以阻塞在状态
                notifyAll();
                if (numable<100){
                    System.out.println(Thread.currentThread().getName() + ":" + numable);
                    numable++;
                    try {
                        //进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }
        }
    }

 A线程进来-->notifyAll()唤醒其他线程,但是A线程进来的时候就拿到锁了,所以 其他线程是就绪状态,等到A线程wait()进入阻塞时会释放锁,然后其他线程中的一个线程拿到锁 如此循环

  1. wait()执行此方法进入阻塞状态,释放同步监视器
  2. notify(),执行此方法,唤醒一个wait()的线程,如果有多个就唤醒优先级高的
  3. notifyall(),唤醒所以的线程;

说明:

  1. 这个三个方法必须在同步代码块中 调用者必须是同步代码块或者同步方法中的同步监视器,否则出现异常
  2. 这三个方法是在Object类中的;

sleep 与 wait()的异同

相同:

执行了的话,都可以使用当前线程进入阻塞状态

不同:

  • 声明的位置不同,Thread类中的sleep(),Object()中的wait()
  • 调用要求不同,sleep()可以在任何需要的场景下使用,而wait()必须使用在同步代码块中
  • 关于是否释放同步监视器,如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器,而wait()会释放

死锁问题 

死锁是什么?

多个线程各自占有一些共享资源,并且互相等待其他线程占用的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止的情形,某一个同步块同时拥有”两个以上对象的锁“时就可能会发生“死锁”的问题。

死锁避免方法

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。

  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺。

  • 循环等待条件:若干进程之间形成一种头尾相连的循环等待资源关系。

以上条件我们只需要破解其中任意一个或者多个条件就能避免死锁的发生。

解决方式一是使用同步代码块时里面不能包含其他的同步代码块(也就是说不能独占其他线程所需资源的锁)

public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append('a');
                    s2.append(1);

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2){
                        s1.append('b');
                        s2.append(2);
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append('c');
                    s2.append(3);

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1){
                        s1.append('d');
                        s2.append(4);
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }

这里是会发生死锁的,因为第一个线程执行后一直拿着 "S2" 这个锁,而第二个线程执行后一直拿着 "S1"这个锁,也就是说都互相拿着对方的锁不放;所以就是一直僵持着;那么这个例子的解决办法就是把包含在同步代码块的同步代码块移出来,让两个同步代码块同级别;

六、守护线程

  • 线程分为用户线程守护线程

  • 虚拟机必须确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

  • 如:后台记录操作日志,监控内存,垃圾回收等待...

守护线程就是当你有一条A线程在运行,另一条线程B会一直陪着A线程运行,知道A线程处于死亡状态后B线程才停止,这就是守护线程

package xiangThread;

//测试守护线程
public class TestDaemon {
    public static void main(String[] args) {
        YongHu yh = new YongHu();
        God god = new God();

        Thread thread = new Thread(god);
        thread.setDaemon(true); //默认flase表示用户线程,正常线程都是用户线程
        thread.start();

        new Thread(yh).start();
    }
}

//你(用户线程)
class YongHu implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 36500; i++) {
            if (i>=36500){
                System.out.println("你离开了这个世界!");
            }
            System.out.println("这是第---"+i+"天");
        }
    }
}

//上帝(守护线程)
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("上帝一直守护着你");
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值