17 Java 中的线程、同步问题

线程、同步

一、线程

1. 并发与并行

  • 并发:指两个或多个事件在同一时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9pJay0hv-1589461159618)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%B9%B6%E8%A1%8C.bmp)]

2. 进程概念

  • **进程:**是指一个内存中运行的应用程序。

3. 线程概念

  • **线程:**线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程是可以有多个线程的,这个应用程序也可以称为多线程程序。

4. 线程调度

  • **分时调度:**所有线程轮流使用cpu的使用权,平均分配每个线程占用的cpu的时间。
  • 抢占式调度:优先让优先级高的线程使用cpu,如果线程的优先级相同,那么会随机选择一个,Java使用的为抢占式调度。

5. 主线程

  • **主线程:**执行主(main)方法的线程。
  • 单线程程序:java程序中只有一个线程,执行从main方法开始,从上到下依次执行。
  • JVM执行main方法,main方法会进入到栈内存,JVM会找操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字就是main(主)线程。

6. 创建多线程的第一种方式:创建Thread类的子类

  • java.lang.Thread类:是描述线程的类,想要实现多线程程序,就必须继承Thread类。
  • 实现步骤:
    • 创建一个Thread类的子类;
    • Thread类的子类中重写Thread类中的run()方法,设置线程任务;
    • 创建Thread类的子类对象;
    • 调用Thread类中的start()方法,开启新的线程,执行run()方法。
      • void start()是该线程开始执行,Java虚拟机调用该线程的run()方法。
      • 结果是两个线程并发的运行:当前进程(main)和另一个线程(创建的新线程,执行其run方法)。
      • 多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动。
  • java程序属于抢占式调度,哪个线程优先级高,就先执行哪个线程;同一优先级,随机选择一个执行。
public class SubThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run " + i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        SubThread st = new SubThread();
        st.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main " + i);
        }
    }
}

7. 多线程原理:随机性打印结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-leL7JXM8-1589461159621)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E9%9A%8F%E6%9C%BA%E6%80%A7%E6%89%93%E5%8D%B0%E7%BB%93%E6%9E%9C.bmp)]

8. 多线程原理:多线程内存图解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PiXn7Pol-1589461159622)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%86%85%E5%AD%98%E5%9B%BE%E8%A7%A3.bmp)]

9. Thread类的常用方法

  • 获取线程名称的方法

    • 第一种:使用Thread类中的方法getName()
      • String getName()返回线程的名称。可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称。
    • 第二种:static Thread currentThread()返回当前正在执行的线程对象的引用。
    public class SubThread extends Thread {
        @Override
        public void run() {
            //第一种方式
           /* String name = getName();
            System.out.println(name);*/
            //第二种方式
            Thread thread = Thread.currentThread();
            String name1 = thread.getName();
            System.out.println(name1);
    
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            SubThread st1 = new SubThread();
            st1.start();  // thread-x
            SubThread st2 = new SubThread();
            st2.start();  // thread-x
            SubThread st3 = new SubThread();
            st3.start();  // thread-x
    
            Thread thread = Thread.currentThread();
            String name = thread.getName();
            System.out.println(name);  // main
        }
    }
    
  • 设置线程名称的方法

    • 第一种:使用Thread类中的方法setName(name)
      • void setName(String name)改变线程名称,使之与参数name相同。
    • 第二种:创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类给子线程起一个名字。
      • Thread(String name)分配新的Thread对象。
    public class SubThread extends Thread{
    
        public SubThread(){}
    
        public SubThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            SubThread st = new SubThread();
            //第一种方法
            st.setName("朱古力");
            st.start();  // 朱古力
    
            //第二种方法
            new SubThread("猪猪侠").start();  // 猪猪侠
            
        }
    }
    
  • sleep方法

    • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),毫秒数结束之后,线程继续执行。
    public class Test {
        public static void main(String[] args) {
            for (int i = 1; i <= 60; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

10. 创建多线程程序的第二种方式:实现Runnable接口

  • java.lang.Runnable:该接口该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。
  • java.lang.Thread类的构造方法:
    • public Thread(Runnable target)分配新的Thread对象。
    • public Thread(Runnable target, String name)分配新的Thread对象。
  • 实现步骤:
    • 创建一个Runnable接口的实现类;
    • 在实现类中重写Runnable接口的run方法,设置线程任务;
    • 创建一个Runnable接口的实现类对象;
    • 创建Thread类对象,构造方法中传递Runnable接口的实现类对象;
    • 调用Thread类中的start方法,开启新的线程执行run方法。
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run " + i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        new Thread(runnable).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main " + i);
        }
    }
}

11. Thread与Runnable的区别

  • 实现Runnable接口创建多线程程序的好处:
    • 避免了单继承的局限性
      • 一个类只能继承一个类,类继承了Thread类就不能继承其他类。
      • 实现了Runnable接口,还可以继承其他的类,实现其他的接口。
    • 增强了程序的扩展性,降低了程序的耦合性
      • 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离。
      • 实现类中,重写了run方法,用来设置线程任务。
      • 创建Thread类对象,调用start方法,用来开启新线程。

12. 匿名内部类方式实现线程的创建

  • 匿名:没有名字

  • 内部类:写在其他类内部的类

  • 匿名内部类的作用:简化代码

    • 把子类继承父类,覆盖重写父类的方法,创建子类对象合成一步完成。
    • 把实现类实现类接口,覆盖重写接口中的方法,创建实现类对象合成一步完成。
  • 匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。

  • 格式:

    new 父类/接口(){
      覆盖重写父类/接口中的方法
    };
    
public class Test {
    public static void main(String[] args) {
        //父类
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() +" "+  i);
                }
            }
        }.start();

        //接口
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() +" "+ i);
                }
            }
        };
        new Thread(runnable).start();

        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() +" "+ i);
                }
            }
        }).start();
    }
}

二、同步

1. 线程安全问题概述

  • 单线程程序不会出现线程安全问题。
  • 多线程程序,没有访问共享数据,不会产生问题。
  • 多线程程序访问了共享的数据,会产生线程安全问题。

2. 线程安全问题的代码实现

public class Tickets implements Runnable{
    private int tickets = 100;
    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
                tickets--;
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Runnable runnable = new Tickets();
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t0.start();
        t1.start();
        t2.start();
    }
}

3. 线程安全问题出现的原理

  • 线程安全问题是禁止出现的,可以让一个线程在访问共享数据的时候,无论该线程是否失去了cpu的执行权,都让其他的线程等待,等待该线程执行完,其他线程在运行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9MA4MIXB-1589461159623)(https://raw.githubusercontent.com/zhugulii/picBed/master/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98%E4%BA%A7%E7%94%9F%E7%9A%84%E5%8E%9F%E7%90%86.bmp)]

4. 解决线程安全问题:同步代码块

  • 格式:

    synchronized(锁对象){
      可能会出现线程安全问题的代码(访问了共享数据的代码)
    }
    
  • 注意:

    • 通过代码块中的锁对象,可以使用任意的对象。
    • 必须保证多个线程使用的锁对象是同一个。
    • 锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
public class Tickets implements Runnable{
    private int tickets = 100;
    //创建锁对象
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            //使用同步代码块
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
                    tickets--;
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Runnable runnable = new Tickets();
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t0.start();
        t1.start();
        t2.start();
    }
}

5. 同步技术的原理

  • 同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
  • 同步保证了只能有一个线程在同步中执行共享数据,保证了线程安全;但是程序会频繁的判断锁,获取锁,释放锁,程序的效率会降低。

6. 解决线程安全问题:同步方法

  • 使用步骤:

    • 把访问了共享数据的代码块抽取出来,放到一个方法中;
    • 在方法上添加synchronized修饰符。
  • 定义方法的格式:

    修饰符 synchronized 返回值类型 方法名(参数列表){
      可能会出现线程安全问题的代码(访问了共享数据的代码)
    }
    
  • 同步方法的锁对象其实就是:实现类对象(this)。

public class Tickets implements Runnable{
    private int tickets = 100;

    //同步方法
    public synchronized void sell(){
        if (tickets > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
            tickets--;
        }
    }

    @Override
    public void run() {
        while (true) {
                sell();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Runnable runnable = new Tickets();
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t0.start();
        t1.start();
        t2.start();
    }
}

7. 静态同步方法

  • 格式:

    修饰符 static synchronized 返回值类型 方法名(参数列表){
      可能会出现线程安全问题的代码(访问了共享数据的代码)
    }
    
  • 静态的同步方法:锁对象不是thisthis是创建对象之后产生的,静态方法优先于对象),是本类的class属性 --> class文件对象(反射)。

8. 解决线程安全问题:Lock锁

  • java.util.concurrent.Locks.Lock接口
  • Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
  • Lock接口中的方法:
    • void lock()获取锁
    • void unlock()释放锁
  • java.util.concurrent.locks.ReentrantLock implements Lock
  • 使用步骤:
    • 在成员位置创建一个ReentrantLock对象;
    • 在可能会出现安全问题的代码前调用Lock接口中的方法lock()获取锁;
    • 在可能会出现安全问题的代码后调用Lock接口中的方法unlock()释放锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Tickets implements Runnable{
    private int tickets = 100;
    //创建ReentrantLock对象  多态
    Lock rl = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
                //调用lock方法
                rl.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 正在卖第 " + tickets + " 张票!");
                    tickets--;
                }
                //调用unlock方法
                rl.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Runnable runnable = new Tickets();
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);

        t0.start();
        t1.start();
        t2.start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值