• JavaSE之多线程、线程安全问题和synchronized代码块


    并发和并行

    并行:在同一时刻,多个指令在多个CPU上同时执行

    并发:在同一时刻,多个指令在单个CPU上交替执行

    进程和线程

    进程指在内存中运行的应用程序。

    每个进程都有自己独立的一块内存空间。在Windows系统中,一个运行的应用程序就是一个进程。

    线程是进程的执行单元,是CPU调度的最小单位。

    一个进程可以由多个线程组成,线程共享进程的所有资源。

    一个进程如果有多个线程在执行,则称为多线程程序

    多线程并发原理

    计算机中的CPU,在任意时刻只能执行一条机器指令。

    每个线程只有获得CPU的使用权才能执行代码。

    线程调度方式

    分时调度

    所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。

    抢占式调度

    优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机一个线程执行,Java使用的是抢占式调度。

    实现线程的方式

    方法一:继承Thread类

    步骤:
    1.继承Thread类

    2.重写run方法,方法体中写要执行的语句

    3.创建自定义线程类对象

    4.调用start方法,启动线程。JVM会自动执行run方法

    //自定义线程类
    public class MyThread extends Thread{
        //1.继承Thread类
        //2.重写run方法,方法体中写要执行的语句
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("thread"+i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    public static void main(String[] args) {
            //3.创建自定义线程类对象
            MyThread thread = new MyThread();
            //4.调用start方法,启动线程。JVM会自动执行run方法
            thread.start();
    
            //main线程继续执行
            for (int i = 0; i < 10; i++) {
                System.out.println("main"+i);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    start方法和run方法的区别:

    start是启动线程的方法,线程启动后会自动执行run方法

    run方法就是线程要执行的代码

    方法二实现Runnable接口

    步骤:
    1.自定义类实现Runnable接口(也叫做任务类)

    2.重写接口的run方法

    3.创建任务类的对象

    4.创建Thread类的对象,把任务对象作为构造方法的参数。

    5.调用start方法,启动线程

    //1.自定义类实现Runnable接口(也叫做任务类)
    public class MyRun implements Runnable{
        //2.重写接口的run方法
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("runable"+i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    public static void main(String[] args) {
            //3.创建任务类的对象
            MyRun myRun = new MyRun();
            //4.创建Thread类的对象,把任务对象作为构造方法的参数。
            Thread thread = new Thread(myRun);
            //5.调用start方法,启动线程
            thread.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    两种方式对比

    方式优点缺点
    继承Thread类编程比较简单,子类中可以直接使用Thread类中的方法扩展性较差,子类不能再继承其他的类
    实现Runnable接口扩展性强,任务类实现接口后还可以继承其他的类编程相对复杂,任务类不能直接使用Thread类中的方法

    线程常用方法

    项目Value
    String getName()获取当前线程名称
    void setName(String name)设置线程名称
    static Thread currentThread()获取当前正在执行的线程对象
    static void sleep(long time)让线程休眠指定的时间,单位为毫秒(休眠时让出CPU执行权)
    void setPriority(int newPriority)设置线程优先级(1-10个等级,10为最高,5为默认优先级)
    int getPriority获取当前线程优先级
    public class MyRun implements Runnable{
        @Override
        public void run() {
            //获取当前线程对象
            Thread t = Thread.currentThread();
    
            //获取当前线程名字
            String name = t.getName();
            for (int i = 0; i < 10; i++) {
                System.out.println(name+i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    public static void main(String[] args) {
            MyRun myRun = new MyRun();
            Thread thread = new Thread(myRun);
    
            //设置线程的名称
            thread.setName("线程R");
    
            thread.start();
    
            //main线程的名称
            Thread t = Thread.currentThread();
            String name = t.getName();
            System.out.println(name);//main方法的线程名称就叫做main
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    线程优先级:
    线程优先级从低到高有1-10级,通常CPU会优先执行优先级较高的线程任务

    但这也不是绝对的,因为线程执行还是有随机性的,只是概率上来说优先级越高的线程越有机会先执行。

    public static void main(String[] args) {
            //新建两个线程
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("t1"+i);
                    }
                }
            });
    
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("t2"+i);
                    }
    
                }
            });
            //打印线程优先级
            System.out.println(thread1.getPriority());
            System.out.println(thread2.getPriority());
            //设置优先级
            thread1.setPriority(1);
            thread2.setPriority(10);
            
            thread1.start();
            thread2.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    线程安全问题

    当多个线程访问共享数据,且多个线程对共享数据有更新操作时,就容易出现线程安全问题。

    Java中提供了同步机制来解决线程安全问题,实现有三种:

    同步机制:对操作共享数据的代码加锁,解决线程安全问题

    1.同步代码块
    格式:
    synchronized(tongbusuo){
    有线程安全问题的代码
    }

    原理:
    在多线程环境下,多个线程会抢占同步代码块中的锁对象。当一个线程获取锁,就可以成功进入同步代码块,其他线程获取不到锁,需要在同步代码块外面等待(阻塞)。

    获得锁的线程执行完同步代码块后会释放锁,此时所有等待的线程会重新争夺锁对象(释放锁的线程也会再次参与争夺)。

    抢到锁的线程就可以进行同步代码块执行。

    实例:模拟卖票

    //模拟窗口卖票
    public class Ticket implements Runnable{
        private static int ticket = 100;
        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            String name = thread.getName();//获取线程名字
    
            //卖票
            while (true) {
                //同步代码块
                synchronized(Ticket.class){
                    //在synchronized中存入唯一的对象
                    if (ticket > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //假设只剩最后一张票,在票数--之前,这时票数还是1符合判断,又会导致很多线程进入。
                        ticket--;
                        System.out.println(name + "成功售票,还剩" + ticket);
                    } else {
                        break;
                    }
                }
    
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    public class TicketDemo {
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Ticket(),"线程1");
            Thread thread2 = new Thread(new Ticket(),"线程2");
            Thread thread3 = new Thread(new Ticket(),"线程3");
    
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    出现线程安全问题。

    在这里插入图片描述
    加上同步代码块后:
    在这里插入图片描述

    2.同步方法

    //模拟窗口卖票
    public class Ticket implements Runnable {
        private static int ticket = 100;
    
        @Override
        public void run() {
            //卖票
            while (true) {
                sale();
                if (ticket == 0) {
                    break;
                }
            }
        }
    
        //定义同步方法,把有安全问题的代码写在方法中
        public synchronized void sale() {
            //同步方法的锁对象是自动提供的
            // 成员方法:锁是this,要保证唯一,得确保当前Ticket类只创建一个对象,若创建多个,无法保证线程安全
            //静态方法:锁是当前类字节码Ticket.class
            Thread thread = Thread.currentThread();
            String name = thread.getName();//获取线程名字
            if (ticket > 0) {
                //假设只剩最后一张票,在票数--之前,这时票数还是1符合判断,又会导致很多线程进入。
                ticket--;
                System.out.println(name + "成功售票,还剩" + ticket);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    同步代码块和同步方法区别:

    同步方法是锁住方法中所有代码,同步代码块可以锁定指定代码,锁的控制粒度更细

    同步方法不能指定锁对象,同步代码块可以指定锁对象

    建议使用同步代码块,更加灵活

    3.Lock锁机制

    JDK1.5开始,并发包(java.util.concurrent)中新增了Lock接口和相关实现类来实现锁的功能,它提供了与synchronized关键字类似的同步功能,Lock中提供了获得锁和释放锁的方法:

    void lock(): 获得锁

    void unlock():释放锁

    Lock接口的常用实现类为ReentrantLock

    //模拟窗口卖票
    public class Ticket implements Runnable {
        private static int ticket = 100;
        private static Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            String name = thread.getName();//获取线程名字
    
            //卖票
            while (true) {
                //加锁
                //发现程序跟上面两种方式有点区别
                //当前程序不会停止,因为锁上之后执行if判断语句,如果执行if可以解锁,
                //如果进入else会导致无法解锁,这个程序就停住了,我们在else中也写上unlock
                //结论:一定要解锁
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //假设只剩最后一张票,在票数--之前,这时票数还是1符合判断,又会导致很多线程进入。
                    ticket--;
                    System.out.println(name + "成功售票,还剩" + ticket);
                } else {
                    lock.unlock();
                    break;
                }
                //解锁
                lock.unlock();
    
                //可以用finally使代码更简便
                
                /*try {
                    if (ticket > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //假设只剩最后一张票,在票数--之前,这时票数还是1符合判断,又会导致很多线程进入。
                        ticket--;
                        System.out.println(name + "成功售票,还剩" + ticket);
                    } else {
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }*/
    
    
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    最后

    如果你对本文有疑问,你可以在文章下方对我留言,敬请指正,对于每个留言我都会认真查看。

  • 相关阅读:
    ZK和redis中是否会发生脑裂问题?
    windows环境下搭建redis5.x集群
    【C++】C++ 引用详解 ⑨ ( 常量引用初始化 | C / C++ 常量分配内存的四种情况 )
    base64转为blob,然后转成file文件,具体步骤以及注释说明,以及使用案例
    PyG-GAT-Cora(在Cora数据集上应用GAT做节点分类)
    C#:实现分枝绑定背包求解器算法(附完整源码)
    Vue3 - 不再支持 IE11,到底为什么?
    【arm扩容】docker load -i tar包 空间不足
    Js中判断true或false
    基于springboot的暖暖午托管理系统
  • 原文地址:https://blog.csdn.net/weixin_47543906/article/details/127769795