多线程基础详解(看到就是赚到)

本文详细介绍了Java中创建线程的不同方式,如继承Thread、实现Runnable接口和使用lambda表达式,以及线程启动、中断、等待、状态和安全机制。重点讲解了死锁的产生条件及解决办法,以及volatile关键字在内存可见性问题中的应用。
摘要由CSDN通过智能技术生成

  • 🎥 个人主页:Dikz12
  • 📕格言:那些在暗处执拗生长的花,终有一日会馥郁传香
  • 欢迎大家👍点赞✍评论⭐收藏

目录

 创建线程

 1.创建类继承Thread,重写run()

 2.实现Runnable,重写run()

3.继承Thread,使用匿名内部类

 4.使用lambda表达式(推荐)

线程启动 

线程中断

1.手动设置标志位

2.使用内部自带的标志位(interrupt)

线程等待 

线程状态 

线程安全

 synchronized(可重入锁) 使用方法

 死锁

关于死锁问题 

死锁能产生,一定涉及到四个必要条件 

volatile关键字(解决内存可见性问题) 


 创建线程

 1.创建类继承Thread,重写run()

class MyThread extends Thread {
    @Override
    public void run() {
        //这个就是线程的入口方法
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread(); //向上转型
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 2.实现Runnable,重写run()

class  MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

3.继承Thread,使用匿名内部类

  /* //实现了Runnable,匿名内部类的写法
      Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });*/
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

 4.使用lambda表达式(推荐)

 public static void main(String[] args) {
        //lambda 表达式 本质上是一个匿名函数,用来实现回调函数
        Thread t = new Thread(() ->{
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

不单单只有上诉这几种,还有其它方式就不在演示了! 

线程启动 

线程启动是通过start(). 而不是run().

run():    只是单纯的描述了当前线程要执行的内容.

start() : 才是真的会调用 系统api,在系统内核上创建线程.

线程中断

1.手动设置标志位

    private static boolean isQuit = false; //成员变量
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("线程开始");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束");
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuit = true;
        System.out.println("设置 isQuit 为 true");
    }

 要注意的是:这里用的是lambda表达式的写法,会发生变量捕获,自动捕获上层域涉及的局部变量.

是有前提限制的,就是只能捕获一个要保证是实际上的final 变量.

2.使用内部自带的标志位(interrupt)

 public static void main(String[] args) {
        Thread t = new Thread(() -> {
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("线程工作中");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   //1.什么都不加, 假装没听见; 继续执行
                   //2.加上break, 表示线程立即结束
                   //break;
                   //3. 可以做一些其它工作,(代码放到这里)执行完之后,在结束
                   break;
               }
           }
            System.out.println("线程结束");
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }

线程等待 

 让一个线程,等待另一个线程执行结束,然后在执行.   本质上就可以理解为控制线程的结束顺序.

join() -> 个哪线程调用,哪个线程就阻塞等待.

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 结束");
        });
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 结束");
        });
        t1.start();
        t2.start();
        System.out.println("主线程结束!");
    }

 

线程状态 

 在Java中,又给线程赋予了一些其它的状态.比如:

NEW: Thread对象已经创建好了,到时start()还没调用.

TERMINATED : Thread对象还在,内核中的线程已经被销毁了.

RUNNABLE: (就绪状态) 线程已经在cpu上执行了/正在排队等待cpu执行.

WAITING(阻塞): 由于wait()引起的阻塞.

TIMED_WAITING: 由于sleep() 引起的阻塞. 

BLOCKED: 由于锁竞争导致的阻塞.

线程状态在调试的时候,可以使用jdk文件下bin目录中的查看线程状态. 

线程安全

 想要解决线程安全问题,就要先了解产生线程不安全的原因.

1.在操作系统中,线程的调度顺序是随机的.(这是由系统内核决定的,除非换个系统)

2.两个线程,对一个变量进行修改

3.修改操作不是原子性的

4.内存可见性问题

5.指令重排序问题

比如:  针对一个变量进行修改.

 private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
           for (int i = 0; i < 50000; i++) {
               //加锁
               synchronized (locker1) {
                   count++;
               }
           }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker1) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        //如果没有这俩 join, 肯定不行的. 线程还没自增完, 就开始打印了. 
        t1.join();
        t2.join();
        //预期结果应该是10W
        System.out.println(count);
    }

 这里引入锁synchronized(可重入锁),作用就是把count 这个变量,成为 原子的, 也就是降低了并发程度.

 synchronized(可重入锁) 使用方法

1.搭配代码块使用

 

2.搭配实例方法或者静态方法

    public int count;
    public void increase () {
        synchronized (this) {
            count++;
        }
    }
    //简化版
    synchronized  public void increase2() {
        count++;
    }
    //静态方法
    public static  void incresae3() {
        synchronized (Fun.class) {

        }
    }
    synchronized public static void increase4() {

    }

 死锁

关于死锁问题 

1.一个线程,针对 同一把锁,连续加锁,如果不是可重入锁,就会发生死锁.(Java中的synchronized是可重入的;C++的std::mutex 就是不可重入锁).

2.两个线程,两把锁.

   线程t1,得到一把锁A 后,又尝试获取锁B;   线程t2 ,得到一把锁B后,有尝试获取锁A.

    public static Object locker1 = new Object();
    public static Object locker2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           synchronized (locker1) {
               // 加上sleep 为了t1 和 t2 线程都能获得一把锁
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (locker2) {
                   System.out.println("t1 加锁成功!");
               }
           }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1) {
                    System.out.println("t1 加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();
    }

 通过调试可以看到,这两个线程进入了BLOCKED的状态(死锁)

这种情况是可以避免的,调整代码结构,上述代码两个synchronized 是嵌套关系,不是并列关系.

3.N 个线程,M把锁.

    典型的例子: 操作系统中的 科学家就餐问题.(就不在详细讨论了) 

死锁能产生,一定涉及到四个必要条件 

1. 互斥使用(锁的基本特性): 一个线程得到一把锁之后,另一个线程也想得到这把锁,就要阻塞等待.

2.不可抢占(锁的基本特性): 一把锁已经被一个线程得到后,另一个线程只能等该线程主动释放,不能强行抢占.

3.请求保持 : 一个线程想获取多把锁(例子:死锁问题的第二个).

4.循环等待/ 环路等待: 线程之间的等待关系成环了. (例子:科学家就餐问题)

所以,解决死锁问题,只要破坏上述四个条件中的其中一个就可以.

1和2,是锁的基本特性,是破坏不了的,也就破坏这两个中的其中一个.

破坏3 : 只需要调整代码结结构,避免出现"嵌套" 逻辑.

破坏4: 约定加锁的顺序,就可以避免循环等待.

volatile关键字(解决内存可见性问题) 

    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() ->  {
           while(isQuit == 0) {
               //循环体
           }
            System.out.println("t1 退出");
        });
        t1.start();
        Thread t2 = new Thread(() ->  {
            System.out.println("请输入isQuit的值:");
            Scanner scanner = new Scanner(System.in);
             isQuit = scanner.nextInt();
        });
        t2.start();
    }

上述代码 运行效果:

期望的结果是,输入1,线程结束. 而这里并没有结束.

 就需要站在cpu的分析下,整个数据的过程:

1.load 读取内存的isQuit值放到寄存器里

2.通过cmp指令比较寄存器得值是否等于0,决定是否要继续执行

读取内存的速度就已经是非常快的了,而读取寄存器的速度是 读取内存速度的 几千倍 几万倍.

所以,Java的编译器就自主做了一个大胆的决定,编译优化,只有第一次循环的时候,才读了内存,后面都是读取寄存器.

解决方案就是通过 volatile 关键字,告诉编译器不要优化!!!

 

 

 

 

 

 

 

 

评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值