多线程(1)-初阶

一、线程与进程
进程:系统分配资源的最小单位。
单进程程序 -》多进程程序(缺点:不能共享资源),故此有了线程。
线程:是系统调度的最小单位。(CPU的调度)。轻量级“进程”。
优点:创建的时候占用更少的资源,并且多个线程之间可以共享资源。
线程共享的资源:1、打开的文件
2、共享内存(new object)
线程与进程的区别:1、进程是系统分配资源的最小单位,线程是系统调度的最小单位
2、一个进程中至少包含一个线程。
3.线程必须要依附于进程,线程是进程实质工作的一个最小单位。

二、线程可以做什么?
1.使用线程休眠来实现电脑字幕打印的功能。

public static void main(String[] args) throws InterruptedException {
        String content = "每个人都会经过一个阶段,见到一座山,就想知道山后面是什么。我很想告诉他,可能翻过山后面,你会发现没什么特别。回望之下,可能会觉得这一边更好。";
        for (char item:content.toCharArray()) {
            System.out.print(item);
            //休眠300ms
            Thread.sleep(100);
        }
    }

2.单线程和多线程的程序对比

public class ThreadDemo2 {
    private static final long count = 5_0000_0000L;
    public static void main(String[] args) throws InterruptedException {
        //多线程方法
        multithreading();
        //单线程方法
        singlethread();
    }

    //单线程
    private static void singlethread() {
        long stime = System.currentTimeMillis();

        int a = 0;
        for (int i = 0; i < 3*count; i++) {
            a++;
        }

        long etime = System.currentTimeMillis();
        System.out.println("单线程执行的时间:"+(etime-stime));
    }

    //多线程
    private static void multithreading() throws InterruptedException {
        long stime = System.currentTimeMillis();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a++;
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int b = 0;
                for (int i = 0; i < count; i++) {
                    b++;
                }
            }
        });
        t2.start();

        int c = 0;
        for (int i = 0; i < count; i++) {
            c++;
        }
        //等待t1,t2执行完
        t1.join();
        t2.join();

        long etime = System.currentTimeMillis();
        System.out.println("多线程执行的时间:"+(etime-stime));
    }
}


三、线程的创建方式
1.继承Thread类
方法一:

public class ThreadDemo3 {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程的名称:"+Thread.currentThread().getName());
        }
    }
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
        System.out.println("线程的名称(主线程):"+Thread.currentThread().getName());
    }
}

方法二:

public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                System.out.println("线程名:"+Thread.currentThread().getName());
            }
        };
        thread.start();
    }

继承Thread类的方式的缺点:在java语言当中只能实现单继承,如果继承了Thread类,也就不能继承其他类。
2.实现Runnable接口的方式
方法一:

static class MyRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("线程名:"+Thread.currentThread().getName());
        }
    }
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }

方法二(主流):

public static void main(String[] args) {
        //匿名内部类的方式实现线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程名:"+Thread.currentThread().getName());
            }
        });
        thread.start();
    }

方法三(jdk1.8主流):

public static void main(String[] args) {
        //lambda+匿名runnable的实现方式
        Thread thread = new Thread(() -> {
            System.out.println("线程名:"+Thread.currentThread().getName());
        });
        thread.start();
    }

3.实现Callable接口的方式:可以得到线程执行之后的结果

//创建线程的任务和实现方法
static class MyCallable implements Callable{
        @Override
        public Object call() throws Exception {
            int num = new Random().nextInt(10);
            System.out.println("子线程:"+Thread.currentThread().getName()
                    +"随机数:"+num);
            return num;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);

        Thread thread = new Thread(futureTask);
        thread.start();
        int result = futureTask.get();
        System.out.println(String.format("线程名: %s, 结果: %d"
                ,Thread.currentThread().getName(),result));
    }

四、线程休眠
方法一:(精度小,时间短)

方法二:(时间长)

方法三:

五、面试题:使用两个线程打印"AABBCCDD"

public static void main(String[] args) {
        String str = "ABCD";
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (char item:str.toCharArray()){
                    System.out.print(item);
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }


六、Thread常见的构造方法
1.创建线程对象

2.使用Runnable对象创建线程对象

3.使用Runnable对象创建线程对象,并命名

4.线程分组:可以将一类线程归为一组,并且进行线程的打印,查看一组线程的具体行为。

public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("开始起跑:"
                        +Thread.currentThread().getName());
                int num = new Random().nextInt(5)+1;
                try {
                    Thread.sleep(num*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("到终点了:"
                        +Thread.currentThread().getName());
            }
        };
        //定义分组
        ThreadGroup group = new ThreadGroup("百米赛跑一组");
        //创建运动员
        Thread t1 = new Thread(group,runnable,"张三");
        Thread t2 = new Thread(group,runnable,"李四");
        t1.start();
        t2.start();
        group.list();
        //等待选手到达终点 ==0 说明以跑完
        while (group.activeCount() != 0) {
        }
        System.out.println("宣布成绩");
    }


在这里插入图片描述
七、Thread常见的属性

public static void main(String[] args) {
        Thread t1 = new Thread(() -> {

        },"张三");
        System.out.println("线程状态 "+t1.getState());
        t1.start();
        System.out.println("线程状态II "+t1.getState());
        System.out.println("线程ID "+t1.getId());
        System.out.println("线程名称 "+t1.getName());
        System.out.println("线程优先级 "+t1.getPriority());
        System.out.println("线程是否为后台线程 "+t1.isDaemon());
        System.out.println("线程是否存活 "+t1.isAlive());
        System.out.println("线程是否被中断 "+t1.isInterrupted());
    }


1.线程优先级

优先级越高,执行的优先级也越高,执行权也就越大,但是CPU的调度很复杂,也不会严格的按照优先级的排序去执行,但总体来看,还是优先级越高执行的权重也就越高。
2.存活状态
仅仅只是创建了线程类MyThread对象,但是并未启动线程,所以线程处于未活动状态,是false;
方法isAlive()的功能是判断当前的线程是否处于活动状态。什么是活动状态呢?
活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
3.后台线程
线程的分类:(1)后台线程(守护线程):true
(2)用户线程(默认):false
守护线程是用来服务用户线程的
进程退出:没有用户线程运行,进程就会结束。
守护线程使用场景:java垃圾回收器
守护线程注意事项:(1)守护线程的设置必须在调用start()之前。如果设置守护线程在start()之后,则程序会报错,并且设置的守护线程不
能生效。
(2)在守护线程里面创建的线程,默认情况下都是守护线程。

public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 在守护线程的内部创建的线程
            Thread t2 = new Thread(() -> {
            });
            System.out.println("t2 是:" +
                    (t2.isDaemon() == true ? "守护线程" : "用户线程"));
        });
        // 设置为守护线程
        /*t1.setDaemon(true);*/
        t1.start();

        System.out.println("t1 是否为守护线程:" + t1.isDaemon());
    }

public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 在守护线程的内部创建的线程
            Thread t2 = new Thread(() -> {
            });
            System.out.println("t2 是:" +
                    (t2.isDaemon() == true ? "守护线程" : "用户线程"));
        });
        // 设置为守护线程
        t1.setDaemon(true);
        t1.start();

        System.out.println("t1 是否为守护线程:" + t1.isDaemon());
    }


4.线程状态
(1)打印所有的线程状态
在这里插入图片描述


在这里插入图片描述
八、线程的方法
1.start()和run()方法的区别
(1)run属于普通方法,而start方法属于启动线程的方法。(调用start方法启动的是新创建的线程,而调用run方法启动的是main)
(2)run方法可以执行多次,而start方法只能执行一次。
2.线程中断
(1)使用全局自定义的变量来终止线程。(在拿到终止指令之后,需要执行完当前的任务才会终止)

//定义全局自定义变量
    private static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
        //转帐线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!flag) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("正在转帐");
                }
                System.out.println("转帐终止");
            }
        });
        t1.start();
        //终止转帐线程
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(310);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("停止转账,有内鬼");
                flag = true;
            }
        });
        t2.start();
        t1.join();
        t2.join();
    }


(2)使用线程提供的方法interrupt来终止线程。(会在收到终止指令之后,会立马结束执行)作用:将线程中的终止状态从默认的false改为true.

public static void main(String[] args) throws InterruptedException {
        //转账线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!Thread.interrupted()) {//判断线程的终止状态
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                    System.out.println("正在转帐");
                }
                System.out.println("转帐终止");
            }
        });
        t1.start();

        Thread.sleep(310);
        System.out.println("有内鬼,终止转帐");
        t1.interrupt();//终止线程
    }

public static void main(String[] args) throws InterruptedException {
        //转账线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //while (!Thread.currentThread().isInterrupted())          //拿到了当前线程的interrupt状态,不会进行复位
                while (!Thread.interrupted()) {//判断线程的终止状态  //拿到全局的interrupt状态,用过一次之后会自动复位(恢复原来的状态)
                    System.out.println("正在转帐");
                }
                System.out.println("转帐终止");
            }
        });
        t1.start();

        Thread.sleep(10);
        System.out.println("有内鬼,终止转帐");
        t1.interrupt();//终止线程
    }


3.等待一个线程

thread.join();
4.获取当前线程的引用

5.休眠当前的线程
Thread.sleep(long millis);
Thread.sleep(long millis,int nanos);//精度到纳秒
6.yield()
出让CPU执行权,特点:不一定能够正常出让CPU的执行权。
九、多线程带来的风险-线程安全问题
1.线程不安全:多线程执行中,程序的执行结果和预期不相符。
2.线程不安全的原因
(1)CPU抢占式执行。(万恶之源)
(2)原子性
(3)编译器优化(代码优化、指令重排序):编译器优化在单线程下没问题,可以提升程序的执行效率,但是在多线程下就会出现混乱(编译器优化的本质是修改代码的执行顺序)从而导致线程不安全的问题。
(4)(内存)可见性
(5)多个线程修改了同一个变量
3.volatile:轻量级解决“线程安全”的方案

volatile的作用:
(1)禁止指令重排序
(2)解决线程可见性的问题:当操作完变量之后,强制删除掉线程工作内存中的此变量。
注意事项:volatile不能解决原子性的问题
(3)在关键代码让CPU排队执行,加锁。
锁操作的关键步骤:
(1)尝试获取锁(如果成功拿到锁则加锁,否则排队等待)
(2)释放锁
4.java中解决线程安全问题的方案
(1)锁(让多线程排队执行)
(2)使用私有变量。
5.锁的解决方案
(1)synchronized加锁和释放锁【JVM层面的解决方案,自动帮我们进行加锁和释放锁】

private static int num = 0;
    private static final int MAXSIZE = 100000;

    public static void main(String[] args) throws InterruptedException {
        //创建一把锁
        Object lock = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    //实现加锁
                    synchronized (lock) {
                        num++;
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    //实现加锁
                    synchronized (lock) {
                        num--;
                    }
                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println(num);
    }


注意事项:在加锁操作时,同一组业务一定是同一组锁对象。
synchronized把锁存在了lock的对象头里面
对象头信息:

互斥锁结构体mutex:

synchronized实现原理:1)从操作系统层面上来说:互斥锁mutex
2)JVM:帮我们实现的监控器锁的加锁和释放锁操作。
3)JAVA:a)锁对象
b)锁存放的地方:变量的对象头
JDK 6 对synchronized进行优化(锁升级):
(2)Lock手动锁【程序员自己进行加锁和释放锁】

private static int num = 0;
    private static final int MAXSIZE = 100000;

    public static void main(String[] args) throws InterruptedException {
        //1,创建手动锁
        Lock lock = new ReentrantLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    //2.加锁
                    lock.lock();
                    try {
                        num++;
                    } finally {
                        //3.释放锁
                        lock.unlock();
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < MAXSIZE; i++) {
                    //2.加锁
                    lock.lock();
                    try {
                        num--;
                    } finally {
                        //3.释放锁
                        lock.unlock();
                    }

                }
            }
        });
        t2.start();

        t1.join();
        t2.join();
        System.out.println(num);
    }

Lock的使用:一定要把lock()放在try外面(1.如果将lock()方法放在try里面,那么就当try里面的代码出现异常之后,就会执行finally里面释放锁的代码,但这个时候加锁还未成功。2.执行finally里面释放锁的代码就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本)

6.锁
(1)synchronized和Lock的区别
synchronized的锁机制时非公平锁
公平锁可以按顺序执行,而非公平锁执行的效率更高(抢占式)。
在java中所有锁默认的策略都是非公平锁。
Lock默认的所策略也是非公平锁,但是Lock可以显示的声明为公平锁。

(2)公平锁实现打印“AABBCCDD”

public static void main(String[] args) throws InterruptedException {
        //声明一个公平锁
        Lock lock = new ReentrantLock(true);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (char item:"ABCD".toCharArray()) {
                    lock.lock();
                    try{
                        System.out.print(item);
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        Thread.sleep(10);
        t1.start();
        t2.start();
    }

(3)synchronized的使用场景:
a)使用synchronized来修饰代码块(加锁对象可以自定义)
b)使用synchronized来修饰静态方法(加锁对象时当前类的对象)

c)使用synchronized来修饰普通方法(加锁对象时当前类的实例)

而Lock只能修饰代码块
十、面试题:volatile和synchronized的区别
volatile可以解决内存可见性问题和禁止指令重排序,但volatile不能解决原子性问题;synchronized是用来保证线程的安全,也就是synchronized可以解决而温暖和关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作)。
十一、面试题:synchronized和Lock之间的区别?
a)synchronized既可以修饰代码块,又可以修饰静态方法或者普通方法;而Lock只能修饰代码块。
b)synchronized只有非公平锁的策略,而Lock既可以是公平锁又可以是非公平锁。
c)ReentrantLock更加的灵活(比如tryLock获取锁失败可以二次执行).
d)synchronized是自动加锁和释放锁的,而ReentrantLock需要自己加锁和释放锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值