多线程笔记

214 阅读7分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

进程与线程

说起进程,就会想到程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。 而进程则是执行的一次执行过程,它是一个动态的概念,是系统资源分配的单位 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程是Cpu 调度和执行的单位。

很多多线程是模拟出来的,真正的多线程是指多个Cpu,即多核,如服务器,如果是模拟出来的多线程,即在一个CPU 的情况下,在同一个时间点,cpu 只能执行一个代码,因为切换的很快,所以就有同时执行的错局

核心 线程就是独立的执行路径 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程; main() 称之为主线程,为系统的入口,用于执行整个程序; 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能被人为的干预的 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制 线程会带来额外的开销,如cpu 调度时间,并发控制开销 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

一共有三种实现的方式

1,Thread 类 2,实现Runnable 接口 3,实现Callable

Thread 类

package com.company;
//方式一,继承 Thread
public class Main extends Thread{
    //重写run 方法


    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我是多线程的"+i);
        }
    }

    public static void main(String[] args) {
	// write your code here
        Main thread = new Main();
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主方法的"+i);
        }
    }
}

结果 在这里插入图片描述

下载网络图片案例

在这里插入图片描述

package com.company;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
//继承 thread
public class down extends Thread{
    private String url;
    private String name;
    public down(String name, String url) {
     this.url = url;
        this.name = name;
    }
 @Override
    public void run() {
        down1 down1 = new down1();
 try {
         down1.down(url,name);
     } catch (Exception e) {
         e.printStackTrace();
     }
     System.out.println("你下载了 = " + name);
 }
//主方法
public static void main(String[] args) {
    down down = new down("1.jpg","https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201312%2F31%2F111859myvyiivetyftfz2n.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1612607756&t=dc789ecb7a82b4d9ef32f324cd8d3104");
    down down1 = new down("2.jpg","https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fsoftbbs%2F1003%2F07%2Fc0%2F3134443_1267900790753_1024x1024soft.jpg&refer=http%3A%2F%2Fimg.pconline.com.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1612607756&t=9c840076d2cd4bbd89e369672a9bcf4c");
    down.start();
    down1.start();
} //写一个下载器
    class down1{
        public void down(String url,String name) throws Exception {
            FileUtils.copyURLToFile(new URL(url),new File(name));
            System.out.println("url = " + url);
            System.out.println("name = " + name);
        }
}
}

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 实现多线程的方式二

package com.company;
//方式二,重写 Runnable
public class Main1 implements Runnable{
    //重写run 方法


    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我是多线程的"+i);
        }
    }

    public static void main(String[] args) {
	// write your code here

        Main1 thread = new Main1();
        new Thread(thread).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("我是主方法的"+i);
        }
    }
}

两者的区别

继承Thread 类 子类继承Thread 类,重写run 方法 启动线程:子类对象.start()

不建议使用:避免OOP 单继承局限性

实现Runnable 实现接口Runnable 具有多线程能力 启动线程:传入目标对象+Thread 对象.start()

推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

卖票的案例

package com.company;

public class ticket implements Runnable{
    //定义票的数量
    private int num=10;
//重写run 方法
    @Override
    public void run() {
        while (true){
            if (num<=0){
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿到了"+num--);
        }

    }

    public static void main(String[] args) {
        ticket ticket = new ticket();
        new Thread(ticket,"娇娇").start();
        new Thread(ticket,"小张").start();
    }
}

这样写容易引起并发的问题 结果会变成这样 在这里插入图片描述 龟兔赛跑的案例

package com.company;
//模拟龟兔赛跑
public class rabbit implements Runnable{
    //定义一个胜利者
    private static String winner;
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            //兔子睡觉
            if (Thread.currentThread().getName().equals("兔子")&&i%10==0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判断比赛是否结束
            boolean flag = end(i);
            if (flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
        }
    }
    //判断是否完成比赛
    private boolean end(int steps) {
        //判断是否有胜利者
        if (winner != null) {  //有胜利者了
            return true;
        }
        {
            if (steps >= 100) {
                winner = Thread.currentThread().getName();
                System.out.println("winner = " + winner);
                return true;
            }

        }
        return false;
    }

    public static void main(String[] args) {
        rabbit rabbit = new rabbit();
        new Thread(rabbit,"兔子").start();
        new Thread(rabbit,"乌龟").start();
    }
}

可以让兔子睡觉,这样胜利者就是乌龟。 实现Callable 多线程

package com.company;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.net.URL;
import java.util.concurrent.*;

//继承 thread
public class down1 implements Callable<Boolean> {
    private String url;
    private String name;
    public down1(String name, String url) {
     this.url = url;
        this.name = name;
    }


    @Override
    public Boolean call() throws Exception {
        down11 down11 = new down11();
        try {
            down11.down(url,name);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("你下载了 = " + name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        down down = new down("1.jpg","https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201312%2F31%2F111859myvyiivetyftfz2n.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1612607756&t=dc789ecb7a82b4d9ef32f324cd8d3104");
        down down1 = new down("2.jpg","https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fsoftbbs%2F1003%2F07%2Fc0%2F3134443_1267900790753_1024x1024soft.jpg&refer=http%3A%2F%2Fimg.pconline.com.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1612607756&t=9c840076d2cd4bbd89e369672a9bcf4c");
        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1 = (Future<Boolean>) ser.submit(down);
        Future<Boolean> r2 = (Future<Boolean>) ser.submit(down1);

        //获取结果
        r1.get();
        r2.get();
        //关闭
        ser.shutdownNow();
    }
    //写一个下载器
    class down11{
        public void down(String url,String name) throws Exception {
            FileUtils.copyURLToFile(new URL(url),new File(name));
            System.out.println("url = " + url);
            System.out.println("name = " + name);
        }
    }
}

在这里插入图片描述 跟上面那个实现下载一个道理

lambda表达式

package com.company;

public class lambdatext {


public static void main(String[] args) {
    //1 lambda 表示简化
    lam la=null;
    la=(int a)->{
        System.out.println("a1 = " + a);
    };
//    2, 简化1,参数类型
    la=(a)->{
        System.out.println("a2 = " + a);
    };
    //简化2,简化括号
    la=a -> {
        System.out.println("a 3= " + a);
    };
//    去掉花括号
    la=a -> System.out.println("a4 = " + a);

    la.demo(100);
}

}
interface lam{
    void demo(int a);
}

理解函数式接口,任何接口如果包含唯一一个抽象方法,那么它就是函数式接口、对于函数式接口,我们可以通过lambda 表达式来创建该接口的对象

总结:lambda 表达式只能由一行代码的情况下才能简化成为一行,前提是接口为函数式接口多个参数也可以去掉参数。

线程状态

线程休眠

sleep(时间) 指定当前线程阻塞的毫秒数 sleep 存在异常 sleep 时间达到后线程进入就绪状态 sleep 可以模拟网络延时,倒计时等 每一个对象都有一个锁,sleep 不会释放锁。

倒计时案例

package com.company;

public class sleep01 extends Thread{
    @Override
    public void run() {
        int i =10;
        while (true){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是倒计时的 = " + i--);
            if (i<=0){
                break;
            }

        }
    }

    public static void main(String[] args) {
        sleep01 sleep01 = new sleep01();
        sleep01.start();
    }
}

获取当前系统时间

package com.company;

import java.text.SimpleDateFormat;
import java.util.Date;

public class sleep02 extends Thread{


    public static void main(String[] args) throws InterruptedException {
        //获取当前系统时间
        Date date = new Date(System.currentTimeMillis());
        while (true){
        Thread.sleep(1000);
        System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
        date = new Date(System.currentTimeMillis());}
    }
}

线程礼让

礼让线程,让当前正在执行的线程暂停,但不阻塞 将线程从运行状态转为就绪状态 让cpu 重新调度。

package com.company;

public class Comity {
    public static void main(String[] args) {
        comity1 comity = new comity1();
        new Thread(comity,"A").start();
        new Thread(comity,"B").start();
    }

}
class comity1 implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"结束");
    }
}


Join

join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

package com.company;

public class Joindemo implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我是主线程 " + i);
        }
    }

    public static void main(String[] args) {
        Joindemo joindemo = new Joindemo();
        Thread thread = new Thread(joindemo);
            thread.start();
        for (int i = 0; i < 300; i++) {

            if (i==100){
                try {
                    thread.join();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("我是main = " + i);
        }
    }
}

线程状态观测

线程状态,线程可以处于一下状态之一: NEW 尚未启动的线程处于此状态 RNNABLE 在JAVA虚拟机中执行的线程处于此状态 BLOCKEO 被阻塞等待监视器锁定的线程处于此状态 WAITING 正在等待另一个线程执行特定动作的线程处于此状态 TIMED_WAIING 正在等待另一个线程执行动作到达指定等待时间的线程处于此状态 TERMINATED 退出的线程处于此状态

一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态

线程优先级

Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照 优先级决定应该调度那个线程来执行 线程的优先级用数字来表示,范围1~10 可以使用一下方式改变获取优先级 getPriority().setPriority(int xxx)

守护线程

线程分为用户线程和守护线程 虚拟机必须确保用户线程执行完毕 虚拟机不用等待守护线程执行完毕 像,后台记录操作日志,监控内存,垃圾回收。

package com.company;

public class Guard {
    //我是主方法
    public static void main(String[] args) {
        guard1 guard1 = new guard1();
        demo demo = new demo();

        Thread thread = new Thread(guard1);
        thread.setDaemon(true);
        thread.start();
        new Thread(demo).start();
    }
}
//守护者
class guard1 implements Runnable{

    @Override
    public void run() {
        while (true){
        System.out.println("我是守护者");}
    }

}   //线程
class demo implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("我是线程"+i);
        }
        System.out.println("我死了!!!");
    }
}

线程同步问题

在多线程问题的时候,多个线程访问同一个对象,并且某个线程还想修改的时候,就需要等待,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕下一个在使用 由于同一个线程的多个线程共享同一块存储空间,会带来访问冲突问题,为了保证数据子啊方法中访问时的正确性,加入了锁机制 当一个线程获得对象的排它锁,独占资源其他线程必须等待,使用后释放锁即可,会有以下问题 1,一个线程持有锁会导致其他所有需要此锁的线程挂起 2,在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题 3,如果一个优先级高的线程等待一个优先级低的线程会导致优先级倒置,引起性能问题

生活中线程不安全的例子

1,上厕所的时候,突然有人拜访,就很没有安全感。 2,在取钱的时候两个人同时操作同一个银行卡的时候,同时看到都有10块的话,不能同时区出来只能一个人拿出来。 3,我们平时在抢票的时候,大家都看到还有10张票,如果都拿到了的话就会出现负数的情况,这样时不安全的。

如何解决不安全的问题

我们可以通过synchronized方法和synchronized块 synchronized方法控制对‘对象’的访问,每个对象对应一把锁每个synchronized方法都必须获得调用该方法的对象的锁才能执行否则线程会阻塞,方法一但执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺点:若将一个打的方法申明为synchronized将会影响效率

同步块

同步块:synchronized(obj){} obj 称之为同步监视器 obj 可以是任何对象,但是推荐使用共享资源作为同步监视器 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class

同步监视器执行过程

1,第一个线程访问,锁定同步监视器,执行其中代码 2,第二个线程访问,发现同步监视器被锁定,无法访问 3,第一个线程访问完毕,解锁同步监视器 4,第二个线程访问,发现同步监视器没有锁,然后锁定并访问 在这里插入图片描述 同步块案例 在这里插入图片描述 这样就就解决了上面买票的不安全了。不会出现负数的情况

不安全的集合arrlist

package com.company;

import java.util.ArrayList;

public class listdemo {
    public static void main(String[] args) {
        ArrayList<Object> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
         new Thread(()->{

                 list.add(Thread.currentThread().getName());
             }).start();

        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }

}

在这里插入图片描述 但是CopyOnWriteArrayList 安全

package com.company;


import java.util.concurrent.CopyOnWriteArrayList;

public class listdemo1 {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Object> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 10000; i++) {
         new Thread(()->{

                 list.add(Thread.currentThread().getName());
             }).start();

        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }

}

点进去源码发现 在这里插入图片描述

死锁

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

package com.company;

public class badlock {
    public static void main(String[] args) {
        lock lock = new lock("娇娇",0);
        lock lock1 = new lock("小张",3);
        lock.start();
        lock1.start();
    }
}
//    鞋子
class Shoes{}
//   裤子
class pants{}
class lock extends Thread{
    //获取资源
    Shoes shoes=new Shoes();
   pants pants=new pants();
//   写选择和穿的人
    int opt;
    String name;

    public lock(String name, int opt) {
        this.name=name;
        this.opt = opt;

    }

    @Override
    public void run() {
        try {
            clothes();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //穿衣服的方法
    public void clothes() throws InterruptedException {
    if (opt==0){
        synchronized (shoes){
            System.out.println("我拿到了鞋"+this.name);
//            Thread.sleep(100);
            synchronized (pants){
                System.out.println("我拿到了裤子"+this.name);
            }
        }
    }
    else {
        synchronized (pants){
            System.out.println("我拿到了裤子"+this.name);
//            Thread.sleep(200);
            synchronized (shoes){
                System.out.println("我拿到了鞋"+this.name);
            }
        }
    }
    }
}

死锁避免方法

产生死锁的条件

1,互斥条件:一个资源每次只能被一个进程使用 2,请求与保持条件,一个进程因请求资源而阻塞时,对以获得资源保证不放 3,不剥夺条件,进程以获得的资源,在未使用完之前,不能强行剥夺 4,循环等待条件,若干进程之间形成一种头尾相接的循环等待资源关系

只要破坏死锁的四个必要条件,就可以了

LOCK锁

java 5.0 开始后,Java 提供了线程同步的机制,通过显示式定义同步锁对象实现同步,同步锁使用lock 对象充当 java.util.concurrent.locks.L ock接口是控制多个线程对共享资源进行访问的工具。 锁提供,了对共享资源的独占访问,每次只能有一个线程对L .ock对象加锁,线程开 始访问共享资源之前应先获得Lock对象 ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语 义,在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释 放锁。

package com.company;

import java.util.concurrent.locks.ReentrantLock;

public class ticket implements Runnable{
    //定义票的数量
    private int num=10;
    ReentrantLock lock = new ReentrantLock();
//重写run 方法
    @Override
    public void run() {

        while (true){
            try {
                lock.lock();
                if (num>0){
                 Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+"拿到了"+num--);
                }else {
                    break;
                }


            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }



        }

    }

    public  static void main(String[] args) {
        ticket ticket = new ticket();
        new Thread(ticket,"娇娇").start();
        new Thread(ticket,"小张").start();
    }
}

synchronized 与lock 的对比 Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域或自动释放 Lock 只有代码块锁,synchronized有代码块锁和方法锁 使用Lock 锁,Jvm 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性 优先使用顺序: Lock > 同步代码块(已经进入了方法体,分配相应资源)>同步方法(在方法体之外)

线程通信

应用场景:生产者消费者问题 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费等待,直到仓库再次放入产品为止

Java 提供了几个方法解决线程之间的通信问题

wait() 表示线程等待,直到其他线程通知,与sleep 不同会释放锁 wait(long timeout) 指定等待的毫秒数 notify() 唤醒一个处于等待状态的线程 notifyAll() 唤醒同一个对象所有调用wait() 方法的线程,优先级别高的线程优先调度

均是Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常

解决方式1 并发协作模式 “生产者/消费者模式” ---》管程法 生产者:负责生产数据的模块(可能是方法,对象,线程,进程); 消费者:负责处理数据的模块(可能是方法,对象,线程,进程); 缓冲区:消费者不能直接使用生产者的数据,他们之间有个‘缓冲区’

生产者将生产好的数据放入缓存区,消费者从缓存区拿出数据

package com.company;

public class PC {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Productor(synContainer).start();
        new Consumer(synContainer).start();
    }
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
    this.container=container;
}
//生产

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new product(i));
            System.out.println("生产了 " + i+"只");
        }
    }
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
    this.container=container;
}

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了 = " + container.pop().id+"只");
        }
    }
}
//产品
class product{
    int id;

    public product(int id) {
        this.id = id;
    }

}
//缓冲区
class SynContainer{
    //需要一个容器大小
    product[] products= new product[10];

    //容器计数器
    int count=0;
    //生产者放入产品
    public synchronized void push(product product){
        //如果容器满了,就需要等待消费消费
        if (count==products.length){
            //通知消费者消费,生产等待
        }
        //如果没有满,我们就需要丢入产品
        products[count]=product;
        count++;
    }
    public synchronized product pop(){
        //判断能否消费
        if (count==0){
            //等待生产者生产
        }
        //如果可以消费
        count--;
       product product= products[count];
       //通知生产者生产
        return product;
    }
}

使用线程池

由于创建和销毁,使用量特别大的资源,比如并发情况下的,会对线程影响大 我们可以创建好多个线程,放入池子里,可以避免频繁创建销毁,实现重复利用,类似生活中的公共交通工具 好处: 提高响应速度(减少了创建新线程的时间) 降低资源消耗(重复利用线程池中线程,不需要每次都创建) 便于线程管理 corePoolSize: 核心池的大小 maximumPoolSize: 最大线程数 keepAliveTime: 线程没有任务时最多保持多长时间后会终止

package com.company;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Pool {
    //1 创建服务,创建线程池
    public static void main(String[] args) {
        //newFixedThreadPool 参数表示的时线程池的大小
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.execute(new MyThread());
        pool.execute(new MyThread());
        pool.execute(new MyThread());
        pool.execute(new MyThread());
        pool.execute(new MyThread());
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

JDK5.0提供了线程池相关API ExecutorService:真的线程池接口。常见子类ThreadPoolExecutor void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执 行Runnable Future submit(Callable task): 执行任务,有返回值,-般又来执行 Callablel void shutdown() :关闭连接池 Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池