一篇看懂Java多线程

java线程安全

一个概念:线程的同步(主要解决线程的安全问题)

问题:

  1. 多个线程执行的不确定性引起执行结果的不稳定
  2. 多个线程对一个内容的共享,会造成操作的不完整性,会破坏数据。

在Java中,通过同步机制来解决线程安全问题。

最初的两种方式:

方式一:同步代码块

synchronized(同步监视器){

//需要同步的代码

}
//说明:操作共享数据的代码,即为需要被同步的代码。
//共享数据:多个线程共同操作的变量。
//同步监视器:俗称:锁。任何一个类的对象都可以充当锁。要求:多个线程必须共用同一把锁。

示例代码:

同步代码块处理实现Runnable接口的线程安全问题:

public class SynchronizedTest {
    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        Thread thread = new Thread(runnableTest);
        Thread thread1 = new Thread(runnableTest);
        Thread thread2 = new Thread(runnableTest);
        thread.start();
        thread1.start();
        thread2.start();
    }

}

class RunnableTest implements Runnable{
    private static int target = 100;

    @Override
    public void run() {
        while (true){
            synchronized(this){//在实现Runnable接口创建多线程的方式中,可以 考虑 使用this充当同步监视器。
                if(target > 0){
                    System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
                    target--;
                }else {
                    break;
                }
            }
        }
    }
}

同步代码块处理继承Thread类的线程安全问题:

public class ThreadTest {
    public static void main(String[] args) {
        ThreadClassTest test = new ThreadClassTest();
        ThreadClassTest test1 = new ThreadClassTest();
        ThreadClassTest test2 = new ThreadClassTest();

        test.start();
        test1.start();
        test2.start();
    }
}

class ThreadClassTest extends Thread{

    private static int target = 100;

    @Override
    public void run() {
        while (true){
            synchronized(ThreadClassTest.class){
                if(target > 0){
                    System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
                    target--;
                }else {
                    break;
                }
            }
        }
    }
}

优点:解决了线程的安全问题

缺点:操作同步代码时只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。

这就是鱼与熊掌不可兼得吧。

方式二:同步方法

  • 如果操作的共享数据完整的声明在一个方法中,就可以将此方法声明为同步的。

示例代码:

同步方法处理实现Runnable接口的线程安全问题:

public class SynchronizedTest1 {
    public static void main(String[] args) {
        RunnableTest1 runnableTest = new RunnableTest1();
        Thread thread = new Thread(runnableTest);
        Thread thread1 = new Thread(runnableTest);
        Thread thread2 = new Thread(runnableTest);
        thread.start();
        thread1.start();
        thread2.start();
    }

}

class RunnableTest1 implements Runnable{
    private int target = 100;

    @Override
    public void run() {
        while (true){
            show();
        }
    }
    public synchronized void show(){// 默认使用的同步监视器是this
        if(target > 0){
            System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
            target--;
        }
    }
}

同步方法处理继承Thread类的线程安全问题:

public class ThreadTest1 {
    public static void main(String[] args) {
        ThreadClassTest1 test = new ThreadClassTest1();
        ThreadClassTest1 test1 = new ThreadClassTest1();
        ThreadClassTest1 test2 = new ThreadClassTest1();

        test.start();
        test1.start();
        test2.start();
    }
}

class ThreadClassTest1 extends Thread{

    private static int target = 100;

    @Override
    public void run() {
        while (true){
            show();
        }
    }
//    public synchronized void show(){// 默认使用的同步监视器时this,有t1,t2,t3三个对象,所以错误
    private static synchronized void show(){//同步监视器就是 ThreadClassTest1.class
        if(target > 0){
            System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
            target--;
        }
    }
}

同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。

非静态的同步方法,同步监视器是:this

静态的同步方法,同步监视器是:当前类本身(类.class)

线程安全之单例模式-懒汉式

同步方法的方式:

class User{

    private User(){}

    private static User user;

    public static synchronized User getUser(){
        if(user == null){
            if(user == null){
                user = new User();
            }
        }
        return user;
    }
}

使用这种方式不太好,因为不管user是不是以及实例化过,在这里全部单线程执行。

同步代码块的方式:

class User{

    private User(){}

    private static User user;

    public static User getUser(){
        if(user == null){
            synchronized (User.class){
                if(user == null){
                    user = new User();
                }
            }
        }
        return user;
    }
}

这里用同步代码块更好,因为如果已经有实例对象了,就可以多线程执行了,全部拿到的同一个对象,不会一股脑阻塞在这里。效率更高

线程的死锁问题

用一段代码演示死锁问题:

public static void main(String[] args) {
    StringBuilder s1 = new StringBuilder();
    StringBuilder s2 = new StringBuilder();

    new Thread(){
        @Override
        public void run() {
            synchronized(s1){
                s1.append("a");
                s2.append("1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(s2){
                    s1.append("b");
                    s2.append("2");

                    System.out.println(s1.toString());
                    System.out.println(s2.toString());
                }
            }
        }
    }.start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized(s2){
                s1.append("c");
                s2.append("3");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(s1){
                    s1.append("d");
                    s2.append("4");

                    System.out.println(s1.toString());
                    System.out.println(s2.toString());
                }
            }
        }
    }).start();
}

在这个代码中第一个线程首先握住s1的锁,然后需要s2的锁才能执行下去,如果这个时候第二个线程握住了s2这个锁,同时需要s1的锁才能往下执行,两个线程所需要的锁都在对方手里,这样就形成了死锁。

官方一点的解释:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

我们使用同步时要避免死锁

避免死锁:

  1. 考虑专门的算法,避开这样的问题
  2. 尽量减少使用同步资源
  3. 尽量避免嵌套同步

方式三:Lock(锁)

jdk1.5提供的一个新特性。

Lock是一个接口,我们使用它的一个典型的实现类:ReentranLock类

示例代码:

Lock(锁)处理实现Runnable接口的线程安全问题:

public class LookTest {

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        Thread thread1 = new Thread(lockTest);
        Thread thread2 = new Thread(lockTest);
        Thread thread3 = new Thread(lockTest);

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class LockTest implements Runnable{
    private int target = 100;

    /**
     *  private ReentrantLock lock = new ReentrantLock(true);
     *  true表示公平,就是线程轮流执行,不会有那种插队的现象
     *  无参构造器默认就是参数时false
     *   private ReentrantLock lock = new ReentrantLock();
     *
     */
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){

            try{
                //调用锁定方法lock()
                lock.lock();
                if(target > 0){
                    System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
                    target--;
                }else {
                    break;
                }
            }finally {
                //调用解锁方法unlock()
                lock.unlock();
            }


        }
    }
}

synchronized和lock的异同

同:都可以解决线程安全问题

异:synchronized在执行完相应的同步代码块之后,自动的释放同步监视器,lock需要手动的去启动同步,同时手动结束同步。明显的lock更加的灵活

sleep() 和 wait()的异同

  • 相同点
    • 一旦执行方法,都可以使得当前线程进入阻塞状态
  • 不同点
    • 两个方法执行的位置不同,Thread类中声明sleep(),Object类中声明wait()
    • 调用的要求不同,sleep()可以在任何需要的场景下调用,wait()只能使用在同步代码块或同步方法中。
    • 如果两个方法都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器(锁),wait()会释放锁。

新增线程创建方式

实现callable接口

jdk5.0新增的。

与Runable相比,Callable更强大些

  • 相比run方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask,比如获取返回结果

Future接口:

  • 可以对具体的Runable,callable任务的执行结果进行取消,查询是否完成,获取结果等。
  • FutureTask是接口Future的唯一实现类
  • FutureTask同时实现了Runnable,Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的值。

示例代码:

public class CallableTest {
    public static void main(String[] args) {
        //3.创建Callable接口实现类对象
        NumTread numTread = new NumTread();

        //4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<>(numTread);

        //5.将FutureTask对象作为参数传递到Thread构造器中,创建Thread对象,并调用start()启动线程。
        new Thread(futureTask).start();

        try {
            //6,获取Callable中call()方法的返回值(看需求)
            //get()的返回值即为FutureTask构造器参数Callable实现类重写call()的返回值
            Integer sum =  futureTask.get();
            System.out.println("总和为:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

//1.创建一个callable的实现类,
class NumTread implements Callable<Integer>{

    //2.实现call方法,将此线程需要执行的操作声明在call中
    @Override
    public Integer call() throws Exception {
        int sum = 0 ;
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

使用线程池

背景:

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

解决办法:

​ 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放入池中。可以避免频繁创建销毁,实现重复利用。

好处:

  • 提高响应速度(减少创建新新线程的时间)
  • 降低资源消耗。(重复利用线程池中的线程,不需要每次都创建)
  • 便于线程管理
    • 线程池大小
    • 最大线程数
    • 线程没有任务时最多保持多长时间终止。

jdk1.5起提供了线程池相关的API:ExecutorService 和 Executors

  • ExecutorService:真正的线程池接口,常见子类ThreadPoorExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task) :执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

示例代码:

public class ThreadPoolTest {

    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //设置连接池的属性
        //强转为ExecutorService接口的实现类
//        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        service1.setCorePoolSize(15);
//        service1.setMaximumPoolSize();

        //执行指定线程的操作。需要实现Runnable接口或Callable接口实现类的对象
        service.execute(new ThreadPoolGetNumber());//一般用来执行Runnable
        service.execute(new ThreadPoolGetNumber());//一般用来执行Runnable
        service.execute(new ThreadPoolGetNumber());//一般用来执行Runnable

//        service.submit();//一般用来执行Callable
        //关闭连接池
        service.shutdown();
    }

}

class ThreadPoolGetNumber implements Runnable{
    private static Integer target = 100;

    @Override
    public void run() {
        while (true){
            synchronized(ThreadPoolGetNumber.class){
                if(target > 0){
                    System.out.println("线程:"+Thread.currentThread().getName()+"拿到的数字是:"+target);
                    target--;
                }else {
                    break;
                }
            }
        }
    }
}

以上内容大部分是看康师傅视频时的笔记,详情请看:
尚硅谷康师傅,你值得拥有

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小黑cc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值