您的位置 首页 java

毕业五年,年薪百万,我掌握了这些java最简单粗暴的入门线程方法

大家好!我是老猿,一个热爱技术的程序猿,Java行业入行7年,每天都在学习和分享的路上!

线程

1.简介

大家都知道多线程给我们带来了更好的资源利用和更好的程序响应,所以关于它的简介我就不多阐明了,大家可以自行搜索,我主要讲的就是重点

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程;
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销;
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致;

2.线程创建

线程的创建有三种方式:

  1. 继承 Thread 类,重写 run() 方法
  2. 实现 Runnable 接口,重写 run() 方法
  3. 实现 Callable 接口(可以设置返回值),重写 call() 方法
 //继承Tread类,重写run()方法
public class MyThread extends Thread {
	@Override
    public void run() {
        System.out.println("这是一个继承Thread类的线程。");
    }
    
    public static void main(String [] args) {
        //线程启动需要使用start()方法,而不是run()
        new MyThread().start();
    }
}
  

 //实现Runnable接口,重写run()方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这是一个实现Runnable接口的线程。");
    }
    
    public static void main(String [] args) {
        //Runnable接口创建线程需要通过Thread类
        new Thread(new MyRunnable()).start();
    }
}
  

 //实现Callable接口,重写call()方法
public class MyCallable implements Callable<E> {
    @Override
    public E call() {
        System.out.println("这是一个实现Callable接口的线程。");
        return new E();
    }
    
    public static void main(String [] args) throws ExecutionException, InterruptedException {
        //第一种方法创建Callable线程
        //创建线程池
        ExecutorService ser = Executors.new FixedThreadPool(3);
        //向线程池提交Callable<E>实现类,并返回Future<E>类
        Future<E> future = ser.submit(new MyCallable());
        //通过get()方法获取call()方法返回值
        E e = future.get();
        //关闭线程池
        ser.shutdownNow();
        
        //第二种方法创建Callable线程
        FutureTask<E> futureTask = new FutureTask<E>(new MyCallable());
        new Thread(futureTask).start();
        //若不先通过start()运行,会导致程序无法停止。
        System.out.println(futureTask.get());
    }
}
  

推荐使用实现Runnable接口,因为Java的单继承局限性,而接口可以多继承,方便同一个对象被多个线程使用。

3.线程

  • 创建状态 为new 一个线程。
  • 就绪状态 为线程调用 start() 方法,此时线程立即进入就绪状态,但 不意味着立刻被cpu调度运行
  • 运行状态 为线程被cpu调度运行中,此时获得cpu资源。
  • 阻塞状态 为线程被停止了,如线程调用 sleep() wait() 、同步锁定时,线程进入阻塞状态,即代码不能继续向下执行,在阻塞状态解除后,会重新进入就绪状态,等待cpu调度执行。
  • 死亡状态 为线程被中止、结束,一旦进入死亡状态,就不能再次启动。

3.1线程方法与说明

  • 除此之外,还有JDK提供的 stop() destroy() 方法。【已废弃】
  • 推荐让线程自己停止下来。
  • 建议使用一个标志位进行终止变量,当flag 为 false时,则终止线程运行。

3.2线程休眠sleep()

  • sleep(时间)指定当前线程阻塞的毫秒数;
  • slee()方法为Thread类静态方法;
  • sleep存在受检异常InterruptedException;
  • sleep时间达到后线程进入就绪状态;
  • sleep可以模拟网络延时,倒计时等等;
  • 每一对象都有一个锁,但 sleep不会释放锁
  • 在哪个线程调用sleep方法就会停止哪个线程;如main线程调用thread.sleep(),此时暂停的是main线程而不是thread线程;

3.3线程让步yield()

  • 线程让步,让当前正在执行的线程暂停,但不阻塞;
  • yield()方法为Thread类静态方法, 该方法不会释放锁
  • 将线程从运行状态转为就绪状态;
  • 让cpu重新调度,所以 让步不一定成功 !只是让线程重新争夺cpu资源;

yield()与sleep()区别

  1. sleep方法给其他线程运行机会时不考虑线程的优先级,因此会给低线程优先级运行的机会,而yield方法只会给相同优先级或者更高优先级线程运行的机会
  2. 线程执行sleep()方法后转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程可能在进入可执行状态后马上又被执行
  3. sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常
  4. sleep()方法比yield()方法(跟操作系统相关)有更好的可移植性

3.4线程加入join()

  • 合并线程,待调用join方法的线程执行完成后,再执行其他线程,其他线程阻塞;
  • 可以想象成插队;
  • join存在受检异常InterruptedException;

3.5线程状态State

  • NEW :尚未启动(即尚未调用start())的线程处于此状态;
  • RUNNABLE :正在运行的线程处于此状态(在Java虚拟机中执行的线程);
  • BLOCKED :被阻塞等待监视器锁定的线程处于此状态;
  • WAITING :正在等待另一个线程执行特定动作的线程处于此状态;
  • TIMED_WAITING :正在等待另一个线程执行动作达到指定等待时间的线程处于此状态;
  • TERMINATED :已退出的线程处于此状态;

3.6线程优先级PRIORITY

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行(实际调度还是由cpu调度,优先级只是建议);

线程优先级用数字表示,范围从1~10

  • Thread.MIN_PRIORITY = 1;
  • Thread.MAX_PRIORITY = 10;
  • ​ Thread.NORM_PRIORITY = 5;

使用 getPriority()或setPriority(int newPriority) 改变或获取优先级

3.7守护线程daemon

  • 线程分为 用户线程 守护线程
  • 虚拟机必须确保用户线程执行完毕;
  • 虚拟机不用等待守护线程执行完毕,如:gc;
  • 使用场景如:后台记录操作日志、监控内存、垃圾回收等

通过 setDaemon(boolean on) 设置是否为守护线程(默认为false,不是守护线程);

4.线程同步

4.1同步介绍

由于同一进程的多个线程共享同一块存储空间,在多线程带来方便的同时,也带来了访问冲突问题,为了保证数据在每个线程中都能保证准确性,在访问时加入锁机制 synchronized ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待占用线程释放锁后才可争夺资源。

但锁也会带来一些问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题;

4.2synchronized关键字

synchronized可以修饰方法或代码块,当修饰普通方法时,锁的是当前对象,修饰静态方法时,锁的是整个类,当修饰代码块时, synchronized(this){} 锁的是当前对象, synchronized(this.getClass()) 锁的是整个类。

4.3线程死锁

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

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

  1. 互斥条件 :一个资源每次只能被一个进程使用;
  2. 请求与保持条件 :一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件 :进程已获得的资源,在未使用完之前,不能强行剥夺;
  4. 循环等待条件 :若干进程之间形成一种头尾相接的循环等待资源关系;
 //死锁例子
public class _05ThreadDeadLock {
    static Object knife = new Object();
    static Object fruit = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (knife){
                System.out.println("获得小刀,想要水果");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fruit){
                    System.out.println("获得水果");
                }
            }
            /*
            //解决方法内部锁外置:
            synchronized (fruit){
                System.out.println("获得水果");
            }
            */
        }).start();
        new Thread(() -> {
            synchronized (fruit){
                System.out.println("获得水果,想要小刀");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (knife){
                    System.out.println("获得小刀");
                }
            }
            /*
            //解决方法内部锁外置:
            synchronized (knife){
                System.out.println("获得小刀");
            }
            */
        }).start();
    }
}
/*
运行结果为:
    获得小刀,想要水果
    获得水果,想要小刀
    程序无法停止……
*/

  

4.4Lock接口(锁)

  • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与synchronized关键字相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock(可重用锁),可以显示加锁、释放锁。
 public class _05ThreadLock {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new Ticket()).start();
        }
    }
}

class Ticket implements Runnable {

    private static int ticket = 10;
    private static final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            lock.lock();
            try {
                if (ticket < 1)break;
                System.out.println("第"+ticket--+"张");
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                //使用Lock锁时建议使用trycatch语句的finally保证锁的释放
                lock.unlock();
            }
        }
    }
}
  

Lock与synchronized的对比:

  • Lock是显式锁(需要 手动开启 手动关闭锁 !),synchronized是隐式锁,出了作用域自动释放。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性。(JUC提供了不同的实现类)
  • 优先使用顺序: Lock > 同步代码块 > 同步方法

5.线程协作

经典线程协作:生产者消费者模式。

生产者消费者模式指生产者生产一份产品,消费者消费一份产品,当已经有产品时,需要通知生产者停止生产,等待消费者消费,当没有产品时,需要通知生产者继续生产,消费者则等待产品生产。

面对这种线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。这时,仅有synchronized是不够的,因为synchronized无法实现不同线程间的消息传递(通信)。

5.1线程通信

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

毕业五年,年薪百万,我掌握了这些java最简单粗暴的入门线程方法


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

Lock接口想要完成线程间通信问题,需要通过Condition接口完成,通过lock.newCondition()获得Condition,调用其await()、signalAll()方法。(与wait()、notifyAll()相似)

5.2生产者消费者模式代码实现

 //生产者消费者模式代码实现
public class ThreadProCus {
    public static void main(String[] args) {
        Goods Goods = new Goods();
        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                lockGoods.add();
            }
        },"A").start();
        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                lockGoods.sub();
            }
        },"B").start();
    }
}

class Goods {
    private int num;
    //Lock方式实现
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void add(){
        lock.lock();
        try {
            while (num == 10){
                //产品已满,停止生产
                condition.await();
            }
            num++;
            System.out.println("生产了第"+num+"件商品");
            //生产了产品,通知消费者消费
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void sub(){
        lock.lock();
        try {
            while (num == 0){
                condition.await();
            }
            System.out.println("消费了第"+num+"件商品");
            num--;
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    /*
    //synchronized方式实现
    public synchronized void add() throws InterruptedException {
        while (num == 10){
            //生产者停止
            this.wait();
        }
        num++;
        System.out.println("生产一个,当前num="+num);
        //通知消费者
        this.notifyAll();
    }

    public synchronized void sub() throws InterruptedException {
        while (num == 0){
            //消费者停止
            this.wait();
        }
        num--;
        System.out.println("消费一个,当前num="+num);
        //通知生产者
        this.notifyAll();
    }
    */
}
  

6.线程池

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

这时,为了解决这个问题,可以通过提前创建多个线程,放入线程池中,使用时直接获取,使用后放回池中,来避免频繁创建、销毁,实现重复利用。

线程池的优点

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

线程池可以设置的属性有:corePoolSize(核心池的大小,即常驻线程数)、maximumPoolSize(最大容纳线程数)、keepAliveTime(线程没有任务时最多存活时间)等

从JDK5.0开始,提供了线程池相关API:ExecutorService和Executors。

ExecutorService :真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用于执行Runnable
  • Future submit(Callable task) :执行任务,有返回值,一般用于执行Callable
  • void shutdown():关闭连接池

Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

 //通过线程池创建线程
public class _09ThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new MyThread());
        }
        executorService.shutdown();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
  

等等,还有……

学习建议

很多人担心学了容易忘,这里教你一个方法,那就是 重复学习。

最后

非常感谢各位小哥哥小姐姐们能看到这里,原创不易,如果你觉得本篇实操方法对你有用麻烦帮助 点个关注、点个赞、留言 都是支持(莫要白嫖)! 有补充有疑问欢迎评论区留言!!!!

毕业五年,年薪百万,我掌握了这些java最简单粗暴的入门线程方法

我会每周定期分享6-7篇 操作系统、计算机网络、Java、分布式、数据库等干货精品原创文章 ,我们下篇文章见!

文章来源:智云一二三科技

文章标题:毕业五年,年薪百万,我掌握了这些java最简单粗暴的入门线程方法

文章地址:https://www.zhihuclub.com/185781.shtml

关于作者: 智云科技

热门文章

网站地图