【JUC多线程与高并发】线程基础,java并发程序基础

博客地址(点击即可访问)github源码地址
深刻理解JMM(JAVA内存模型)https://github.com/zz1044063894/JMMprojcet
volatile详解https://github.com/zz1044063894/volatile
线程基础,java并发程序基础https://github.com/zz1044063894/thread-base
线程进阶,JDK并发包https://github.com/zz1044063894/JDK-concurrency
多线程进阶,性能优化之锁优化https://github.com/zz1044063894/lock-optimization
线程进阶,性能优化之无锁https://github.com/zz1044063894/no-lock

线程与进程

进程(Process)

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早起面向进程的计算机结构中, 进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据以及组织形式的描述,进程是程序的实体。

线程(Thread)

线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

联系与区别

简单来说:
进程是一个容器。比如一个家庭(一家三口)的生活,家里有电视、冰箱、洗衣机还有两个卧室。这个大环境来说就可以说是个进程,而三个人就是3个线程。他们可能互相不冲突,比如妈妈在卧室休息,爸爸在看电视,女儿在洗衣服。又可能有冲突,比如女儿在看动画片的时候爸爸就不能看体育频道了。

线程就是轻量级的进程,是程序执行的最小单位,一个进程至少含有一个线程.
使用多线程是因为线程间的切换和调度的成本远远小于进程

线程的基本操作

新建线程

方法1:继承Thread的run方法创建线程:

通过继承Thread类来创建并启动多线程的一般步骤如下

 (1)d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
 
 (2)创建Thread子类的实例,也就是创建了线程对象
 
 (3)启动线程,即调用线程的start()方法

注意:不要用run()来开启新线程,他只会在当前线程中串行执行run()中的代码,即为运行run()方法

class MyThread1 extends Thread {
    public MyThread1(String name) {
        super(name);
    }
    @Override
    public synchronized void run() {
        System.out.println("继承thread使用run()方法启动线程:" + Thread.currentThread().getName());
    }
}
public static void main(String[] args) throws Exception {
        //方法1:
        MyThread1 thread1 = new MyThread1("A");
        thread1.start();
       

}

方法2:实现Runnable的run方法创建线程(常用)

实现Runnable接口创建线程的步骤为:

 (1)创建一个类并实现Runnable接口
 
 (2)重写run()方法,将所要完成的任务代码写进run()方法中

 (3)创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去

 (4)使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程
class MyThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable的run()方法启动线程:" + Thread.currentThread().getName());
    }
}
public static void main(String[] args) throws Exception {
       
        //方法2
        Thread thread2 = new Thread(new MyThread2());
        thread2.start();
    
}

方法3:使用Callable和Future创建线程

实现Callable接口创建线程的步骤为:

(1)创建一个类并实现Callable接口

(2)重写call()方法,将所要完成的任务的代码写进call()方法中,需要注意的是call()方法有返回值,并且可以抛出异常

(3)如果想要获取运行该线程后的返回值,需要创建Future接口的实现类的对象,即FutureTask类的对象,调用该对象的get()方法可获取call()方法的返回值

(4)使用Thread类的有参构造器创建对象,将FutureTask类的对象当做参数传进去,然后调用start()方法开启并运行该线程。
class MyThread3 implements Callable{

    @Override
    public Object call() throws Exception {
        System.out.println("实现callable的call()方法启动线程:"+ Thread.currentThread().getName());
        return null;
    }
}
public static void main(String[] args) throws Exception {
        //方法3
        MyThread3 thread3 = new MyThread3();
        FutureTask futureTask = new FutureTask(thread3);
        new Thread(futureTask).start();
}

方法4:使用线程池例如用Executor框架

使用线程池创建线程的步骤:

(1)使用Executors类中的newFixedThreadPool(int num)方法创建一个线程数量为num的线程池

(2)调用线程池中的execute()方法执行由实现Runnable接口创建的线程;调用submit()方法执行由实现Callable接口创建的线程

(3)调用线程池中的shutdown()方法关闭线程池
class MyThread4 implements Runnable{

    private int num;

    public MyThread4(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        System.out.println("正在执行task " + num );
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task " + num + "执行完毕");
    }

}
public static void main(String[] args) throws Exception {

        //方法4
        //设置核心池大小
        int corePoolSize = 5;
        //设置线程池最大能接受多少线程
        int maximumPoolSize = 10;
        //当前线程数大于corePoolSize、小于maximumPoolSize时,超出corePoolSize的线程数的生命周期
        long keepActiveTime = 200;
        //设置时间单位,秒
        TimeUnit timeUnit = TimeUnit.SECONDS;
        //设置线程池缓存队列的排队策略为FIFO,并且指定缓存队列大小为5
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);
        //创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepActiveTime, timeUnit,workQueue);
        //往线程池中循环提交线程
        for (int i = 0; i < 3; i++) {
            //创建线程类对象
            MyThread4 thread4 = new MyThread4(i);
            //开启线程
            executor.execute(thread4);
            //获取线程池中线程的相应参数
            System.out.println("线程池中线程数目:" +executor.getPoolSize() + ",队列中等待执行的任务数目:"+executor.getQueue().size() + ",已执行完的任务数目:"+executor.getCompletedTaskCount());
        }
        //待线程池以及缓存队列中所有的线程任务完成后关闭线程池。
        executor.shutdown();

}

终止线程(stop

一般来说,线程在执行完毕后就会自动结束,无需手动关闭。但是,凡是都有例外。一些服务端后台线程可能会常驻系统,他们通常不会正常中介。比如,他们的执行体本身就是一个大大的无穷循环,用于提供某些服务。

查阅JDK不嫩发现Thread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程停止,非常方便。但是该方法已经被弃用了。

弃用原因

sto方法太过于暴力了,强行把一些正在执行的线程终止,他可能会造成一些数据不一致的问题。
下面我几个例子说明,假如数据库中维护着一张表,里面记录了用户ID和用户名。假设有两条记录:

记录1:id=1,name=张三;
记录2:id=2,name=李四;

如果我们使用一个User对象去保护这些记录,我们肯定会希望这个User对象完整的存储,如果User对象存储各存一半,我相信大部分人都会头疼。

为什么会出现这种情况呢?

Thread.stop()方法在结束线程时,会直接终止线程,并like释放这个线程锁持有的锁。而这些锁恰恰就是维护对象一致性的。如果此时,写线程写入数据写了一般,并强行终止,那么对象就会被写坏,同事由于锁已经被释放,另一个等待该锁的度线程就读到了不一样的对象。
请看实例代码:

public class StopError {
    public static User user = new User();

    public static class ChangThread extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (user) {
                    int v = (int) (System.currentTimeMillis() / 1000);
                    user.setId(v);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    user.setName(String.valueOf(v));
                }
            }
        }
    }
    public static class ReadThread extends Thread{
        @Override
        public void run() {
            while (true) {
                synchronized (user) {
                   if(user.getId()!=Integer.parseInt(user.getName())){
                       System.out.println(user.toString());
                   }
                }
                yield();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReadThread().start();
        while (true){
            Thread thread = new ChangThread();
            thread.start();
            Thread.sleep(150);
            thread.stop();
        }
    }
}

class User {
    int id;

    public User() {
        this.id = -1;
        this.name = "-1";
    }

    String name;

    public int getId() {
        return id;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

运行结果:
在这里插入图片描述

线程中断

在java中线程中断是一个重要的线程协作机制。从字面上理解,中断就是让目标线程停止执行的意思,而实际上并非完全如此。在上面我们已经讨论过stop方法停止线程的害处。强大的JDK又给我们提供了更方便的方法,那就是线程中断。
严格的来说,线程中断并不会立即退出线程,而是给线程发一个通知,告诉目标线程你应该退出了,至于目标线程后续如何工作,那就是目标现成自行决定了。不立即退出也是中断和停止线程的区别

有关中断的有三个方法,他们看起来像是三胞胎,可能会引起混淆,请大家注意

方法含义
public void Thread.interrupt()中断线程
public boolean Thread.isInterrupted()判断线程是否被中断
public static boolean Thread.interrupted()判断线程是否被中断并清除当前中断状态

下面请看代码并分析,线程会立刻被停止吗?

 public static void main(String[] args) throws InterruptedException {
        int i = 0;
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println(i);
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
}

这里虽然中断了,但是线程并没有对中断处理的逻辑,所以即使线程被中断也不会发生任何作用。如果希望中断后退出,就必须为他加上中断处理代码:

public static void main(String[] args) throws InterruptedException {
        int i = 0;
        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("线程已经中断了");
                        break;
                    }
                    System.out.println(i+1);
                    Thread.yield();
                }
            }
        };
        t2.start();
        Thread.sleep(2000);
        t2.interrupt();
    }
t1线程是一个死循环,一直输出0,
t2线程是一个可以结束的线程,当输出线程被中断后就结束执行

线程休息(sleep)

public static native void sleep(long millis) throws InterruptedException
Thread.sleep方法会让当前线程休息若干时间,它会抛出一个InterruptedException异常。不是运行异常,也就是说程序必须捕获并且处理它,当线程在sleep休眠时,如果被中断,这个异常就会产生。

package com.jingchu.thread.sleep;

/**
 * @description: 休息是中断产生异常实例
 * @author: JingChu
 * @createtime :2020-07-20 12:30:41
 **/
public class MySleep {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("线程被中断了");
                        break;
                    }
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        System.out.println("设置中断状态");
                        Thread.currentThread().interrupt();
                        e.printStackTrace();
                    }

                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }
}

运行结果:
在这里插入图片描述
注意:Thread.sleep()方法由于中断而抛出异常,此时,他会清除中断记忆,如果不加处理,下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标志位。

等待(wait)和通知(notify)

为了支持多线程间的协作,JDK提供了两个非常重要的接口,wait和notify,这两个不是在Thread中而是在Object类,也就意味着任何对象都可以调用这两个方法

public final void wait() throws InterruptedExption
public final native void notify()

当在一个对象实例上调用wait方法后,当前线程就会在这个对象上等待。比如,线程A调用了obj.wait(),那么线程A就会停止继续执行,而转为等待状态。等到到合适结束呢?线程A则会一直等待到其他线程调用了obj.notify()。这时,obj就成了多个线程间有效的通信手段。

当notify被调用时,会从等待队列中随机选择一个线程并将它唤醒,并不是先等待的线程就先有优先权,这个选择完全是随机的。

除了notify方法外,还有一个类似的方法notifyAll,不同的是,这个方法会唤醒在这等待队列中所有等待的线程,而不是随机一个
还需要强调一点就是,Object.wait()并不是可以随便调用的,必须包含在对应的synchronzied语句中,无论是wait()还是notify()都需要首先获得目标对象的监视器。
下面请看案例:

package com.jingchu.thread.communication;

/**
 * @description: 线程通信实例
 * @author: JingChu
 * @createtime :2020-07-20 12:46:09
 **/
public class MyWait {
    final  static Object object = new Object();
    public static class Thread1 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println("线程1启动:");
                try{
                    System.out.println("线程1进入等待");
                    object.wait();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println("线程1结束");
            }
        }
    }
    public static class Thread2 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println("线程2通知线程1可以运行");
                object.notify();
                System.out.println("线程2结束");
                try{
                    Thread.sleep(200);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread1();
        Thread thread2 = new Thread2();
        thread1.start();
        thread2.start();
    }
}

运行结果
在这里插入图片描述
解析:
上述代码中,开启了两个线程,执行了obj.wait()方法。注意:程序在执行wait()方法钱先申请了对象锁。所以wait他是持有obj的锁,wait方法执行后,thread1进行等待,并释放obj的所。thread2在执行notify后也会先获得obj的锁。
注意:Thread.sleep()和wait()都可以让线程等待若干时间。除了wait()可以被唤醒外,另一个主要区别就是wait()方法会释放目标对象的锁。

挂起(suspend )和继续执行(resume

挂起和继续执行是一对相反的操作,被挂起的线程必须要等到resume操作后,才可以继续制定。乍看一下感觉这个很好用,但是其实他们已经被停用了。

弃用原因

因为suspend在导致线程暂停的同时,并不会释放任何锁资源,这就会导致其他任何线程想访问被他暂听使用的锁时,都会被牵连,导致无法正常运行。直到进行了resume()操作,被挂起的线程才能继续,从而其他阻塞在相关锁上的线程也可以继续执行。但是如果resume()操作意外在suspend()前执行了,那么被挂起的线程就很有可能继续执行。并且,更严重的是:它占用的锁不会被释放,这可能导致整个系统工作不正常。
代码示例:

package com.jingchu.thread.suspend;

/**
 * @description: 挂起和继续执行测试
 * @author: JingChu
 * @createtime :2020-07-20 13:04:30
 **/
public class Mysuspend {
    public static Object object = new Object();
    static ChangeThread t1 = new ChangeThread("t1");
    static ChangeThread t2 = new ChangeThread("t2");

    public static class ChangeThread extends Thread {
        public ChangeThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            synchronized (object) {
                System.out.println("线程正在执行:" + Thread.currentThread().getName());
                Thread.currentThread().suspend();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(1000);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}

在这里插入图片描述
虽然我们看到了程序的输出,但是程序并没有结束,这说明线程被挂起了。

等待线程结束(join)和谦让(yield)

很多情况下,一个线程的输入依赖于另外线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来支持这个功能,如下所示,展示了2个join()方法。

public final void join() throws InterruptException
public final syn chronized void join(long millis) throwsInterruptedException

第一个方法表示无限等待,他会一直阻塞当前线程,知道线程执行完毕。第二个方法给出了最大等待时间,如果超出等待时间,会继续往下执行。
下面请看实例代码:

package com.jingchu.thread.join;

/**
 * @description: 线程等待实例
 * @author: JingChu
 * @createtime :2020-07-20 13:14:25
 **/
public class MyJoin {
    public volatile static int i=0;
    public static class AddThread extends Thread{
        @Override
        public void run() {
            for(i=0;i<100000;i++) {

            }
        }

        public static void main(String[] args) throws InterruptedException {
            AddThread at = new AddThread();
            at.start();
            at.join();
            System.out.println(i);
        }
    }
}

结果:
![在这里插入ht描述]外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-(ht![在这里插入图片描述](https://img-blog.csdnimg.cn/20200720131826416.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzg3NjEyMQ==,size_16,color_FFFFFF,t_70)]tps://img-blog.csdnimg.cn/20200720131639864.png)

主函数中如果不使用join等待AddThread,那么输出的值就是0,因为还没等待AddThread执行,就直接输出了。如果使用了join则表示Main线程愿意等待addThread线程执行结束,所以i总是100000

public static native void yield()

这是一个静态方法,一但执行,他会让出CPU,但是需要注意,让出CPU不代表当前线程就不执行了。当前线程会加入到CPU资源的争夺,但是能否被再分配就不一定了。

线程组

在一个系统中,如果线程的数量很多,而且分工能却,我们就可以把相同功能的相乘分成一组。
下面请看例子:

package com.jingchu.thread.group;

/**
 * @description: 线程组测试实例
 * @author: JingChu
 * @createtime :2020-07-20 13:26:18
 **/
public class MyThreadGroup implements Runnable {


    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getThreadGroup().getName()+"-"+Thread.currentThread().getName());
            try{
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadGroup threadGroup = new ThreadGroup("线程组");
        Thread t1 = new Thread(threadGroup,new MyThreadGroup(),"T1");
        Thread t2 = new Thread(threadGroup,new MyThreadGroup(),"T2");
        t1.start();
        t2.start();
        System.out.println(threadGroup.activeCount());
        threadGroup.list();
    }
}

运行结果
在这里插入图片描述

守护线程(Deamon)

守护线程是一个特殊的线程,是系统的守护者,在后台默默的完成系统的服务,比如垃圾回收。用户线程可以当做是系统的工作线程,它会完成系统要完成的业务操作,如果所有的用户线程都结束了,那么守护线程自然也就结束了。

package com.jingchu.thread.deamon;

/**
 * @description: 守护线程
 * @author: JingChu
 * @createtime :2020-07-20 13:31:16
 **/
public class MyDeamon {
    public static class Deamon extends Thread{
        @Override
        public void run() {
            System.out.println("我是一个活跃的线程");
            try{
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Deamon();
        t.setDaemon(true);
        t.start();
        Thread.sleep(2000);
    }
}

注意:设置守护线程必须在线程start()之前,否则你会获得一个异常,告诉你设置守护线程失败

线程的优先级

在Java中线程有自己的优先级,优先级越高的线程在竞争资源师更有优势,但不是说具有绝对性的优势。线程的优先级范围是1-10,数字越大优先级越高。
下面请看例子

package com.jingchu.thread.priority;

/**
 * @description: 线程的优先级测试
 * @author: JingChu
 * @createtime :2020-07-20 13:38:24
 **/
public class MyPriority {
    public static class HPriority extends Thread{
        static int count=0;

        @Override
        public void run() {
            while (true){
                synchronized (MyPriority.class){
                    count++;
                    if(count>1000000){
                        System.out.println("高优先级线程结束");
                        break;
                    }
                }
            }
        }
    }
    public static class Lpriority extends Thread{
        static int count=0;

        @Override
        public void run() {
            while (true){
                synchronized (MyPriority.class){
                    count++;
                    if(count>1000000){
                        System.out.println("低优先级线程结束");
                        break;
                    }
                }
            }
        }
    }
    public static class Mpriority extends Thread{
        static int count=0;

        @Override
        public void run() {
            while (true){
                synchronized (MyPriority.class){
                    count++;
                    if(count>1000000){
                        System.out.println("中优先级线程结束");
                        break;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread high = new HPriority();
        Thread low = new Lpriority();
        Thread mium = new Mpriority();
        high.setPriority(9);
        low.setPriority(3);
        mium.setPriority(6);
        high.start();
        low.start();
        mium.start();
    }
}

绝大部分运行结果都是高中低,但是偶尔有特殊情况。

synchronized

线程安全:
一般来说,程序并行化是为了获取更高的执行效率,但是效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序苯本身就是去了意义。
还记得上一章中我们写了一个10个线程分别执行10000此i++的例子,我们期望的结果是1000000,但是结果却不像我们想的那样。因为可能多个线程同时对i进行写入时,其中一个线程的记过会覆盖另一个。

当时我们提出了两个解决方法,一个是加关键字synchronized,另一个是使用整型原子类。上一章中,我们已经给出了原子类的源码,下面请看加关键字synchronized源码:

package com.jingchu.thread.synchronize;

/**
 * @description: 线程安全测试案例
 * @author: JingChu
 * @createtime :2020-07-20 17:52:17
 **/
public class PlusTask implements Runnable {
    private static int i = 0;

    @Override
    public void run() {
        for (int k = 0; k < 10000; k++) {
            synchronized (PlusTask.class) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int j = 0; j < 10; j++) {
            threads[j] = new Thread(new PlusTask());
            threads[j].start();
        }
        for (int j = 0; j < 10; j++) {
            threads[j].join();
        }

        System.out.println(i);
    }
}

运行结果:
在这里插入图片描述
关键字synchronized有多种用法,这里做一个简单的整理

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁
  • 直接作用于实例方法:相当于给实例方法加锁,进入同步代码前要获得当前实例的锁
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获取当前类的锁
    上面例子相当于给实例代码加锁

并发情况下的不易发现的错误

并发下的ArrayList

代码

我们都知道ArrayList是线程不安全的容器,那么到底会导致什么样的问题呢?请看如下代码:

package com.jingchu.thread.error;

import com.jingchu.thread.communication.MyWait;

import java.util.ArrayList;

/**
 * @description: 并发下的ArrayList案例
 * @author: JingChu
 * @createtime :2020-07-20 18:03:44
 **/
public class MyArrayList {
    static ArrayList<Integer> arrayList = new ArrayList<>(10);
    public static class AddThread implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                arrayList.add(i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AddThread());
        Thread t2 = new Thread(new AddThread());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(arrayList.size());
    }
}

运行结果1:

在这里插入图片描述
这个出现了一个非常隐蔽的错误,就像之前多个线程进行i++类似。

运行结果2:

在这里插入图片描述
这个结果说明程序抛出了异常,这是因为ArrayList在扩容的过程中,内部的一致性被破快,但由于没有锁的保护,另外的线程访问到了不一致的内部状态,导致出现了越界问题

运行结果3:

在这里插入图片描述
这个结果说明,ArrayList的最终大小确实为20000,这说明并发程序有问题,但不一定每次都表现出来

解决方法很简单,使用线程安全的Vectory代替ArrayList即可

并发下的HashMap

HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也能回遇到意想不到的错误,不过和ArrayList不同,HashMap的问题似乎更加诡异。

代码

package com.jingchu.thread.error;

import java.util.HashMap;
import java.util.Map;

/**
 * @description: 并发下的HashMap测试案例
 * @author: JingChu
 * @createtime :2020-07-20 18:12:50
 **/
public class MyHashMap {
    static Map<String, String> map = new HashMap<>();

    public static class AddThread implements Runnable {
        int start = 0;

        public AddThread(int start) {
            this.start = start;
        }

        @Override
        public void run() {
            for (int i = start; i < 10000; i = i + 2) {
                map.put(Integer.toString(i), Integer.toBinaryString(i));

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AddThread(0));
        Thread t2 = new Thread(new AddThread(1));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(map.size());
    }
}

结果

结果内容
10000
小于10000
程序无法停止

对于前两种情况和ArrayList一致,无需多解释,但是第三种,大家一定很惊讶。

分析
打开任务管理器,你们会发现这段代码占用了极高的CPU,可能已经占满了两个CPU。这非常类似于死循环的情况。
当两个线程在遍历HashMap的数据,是一个迭代遍历,就如同在遍历一个列表,由于多线程的冲突,链表的结构遭到了破坏,上述迭代就如同一个死循环。

错误的加锁

初学时,经常出现这个问题。下面请看下面的代码

package com.jingchu.thread.error;

/**
 * @description: 错误加锁问题
 * @author: JingChu
 * @createtime :2020-07-20 18:26:23
 **/
public class MyBadnLocak {
    public static class BadnLocak implements Runnable {
        public static Integer i = 0;
        static BadnLocak instance = new BadnLocak();

        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {

                synchronized (i){
                    i++;
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }


}

我们为了保证技术器i的正确性,每次都i自增前,都先获得i的锁,为了保证i是线程安全的。但是执行结果却不如同我们的预期,我们获得的都是小与20000的值。

解释:
因为Integer属于不变对象,也就是说对象一旦创建了,就不可能被修改。也就是说,如果你有一个Integer代表1,那么它韵苑就是1,你不可能改变Inter的值,使他变成2,如果要使用2怎么办呢,就仙剑了一个Integr,并让他表示2,我们查看Integer.valueOf()源码,如下图。
在这里插入图片描述
实际上他是一个工厂方法,它倾向于返回一个代表数值的Integer实例。
如此一来我们也就清楚了,由于多线程间,并不一定能看到同一个对象,因此,两个线程每次加锁都加在了不同的对象实例上,导致了外面的代码出现了问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值