java并发编程艺术——基础篇

这篇文章目的是为了总结一下这段时间看《java并发编程艺术》学到的东西,尝试用自己的话说出来对java多线程的理解和使用。

一、什么是多线程,为什么要用多线程,多线程带来的挑战

多线程定义
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。–来自百度词条

为什么要使用多线程:总的来说是因为现代计算机硬件的发展导致的,单核cpu性能的瓶颈已经难以突破,那么就在一台计算机上多加cpu呗,现在的个人电脑2核4核 的cpu已经是很普及了,更别说用作服务器的计算机了,所以近年来多线程发展如日中天。
使用多线程带来性能上的提升,什么是性能,我的理解就是同样的时间做的事情多少,比如一个小时的时间学霸可以背60个单词,我就只能记住10个单词,但是如果我能在早上做早餐的时候,比如烧水的时候,我需要等待水烧开,注意这里出现了一个重要信号等待,我的大脑(cpu)空闲下来了,这个时候再继续背单词是不是就能多背几个呢,然后当水烧开时水壶报警音(信号),通知我水烧开了,我继续做早餐。这样我背单词的性能是不是提高了呢?

多线程会出现什么问题
1、上下文切换会带来额外的开支:继续我背单词的例子,我决定我一个早上只要空闲下来就去背单词,于是等待烧水的时候我背了4个半单词,第5个单词我记住了一半前4个字母,这个时候水壶响了,我是不是得放下手中的单词书,然后我还必须记住这本单词书放在哪里,背到了几页,背了几个单词,背到第几个字母(其实这个就是我们的jvm中的程序计数器干的活,记住我们的程序执行到了什么位置,以便下次切换到该线程jvm知道从什么地方执行,程序计数器是线程私有的),这个时候我去倒水做早餐,这就带来了负担,因为我的大脑需要记住额外的东西,当频繁烧水、背单词两个动作切换的时候我需要记住额外的单词书的页码,和没背完的单词字母。但是确实提高了我总体的效率,我之前一个小时只能做早餐,但是我现在多记住了5个单词啊。

2、死锁:`public class DeadLockDemo {
static String A = “A”;
private static String B = “B”;

public static void main(String[] args) {
    new DeadLockDemo().deadLock();
}

private void deadLock() {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (A) {
                try {
                    Thread.currentThread().sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
                    System.out.println("1");
                }
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (B) {
                synchronized (A) {
                    System.out.println("2");
                }
            }
        }
    });
    t1.start();
    t2.start();
}

}`
比如上面的代码,AB互相锁住,造成了死锁,这是我们在进行多线程编程时一定要避免的
下面是书中提到的几个方法:
1、 ·避免一个线程同时获取多个锁。
2、 ·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
3、 ·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
4、 ·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

二、锁

锁是多线程编程中的重点内容,正所谓自由是相对的,有锁才能有自由,因为并发编程涉及到对同一个共享变量的操作,为避免造成变量修改混乱所以加锁是必须的。

1、volatile和synchronized关键字

volatile关键字:
volatile关键字的主要作用是保证共享变量的可见性,当A线程修改了共享变量c字段,B线程启动时,B线程能感知到这个共享变量c变化,借此来进行线程间的通信。

注意:volatile关键字并不能保证原子性,因为它具有的只有可见性,对于i++这种操作相当于是①:先去读i的值
②:将i+1赋值给i
假设i=1在进行①操作结束时②操作未开始,另外一个线程将i的值修改为了5,那么i+1操作依然是1+1(i原先的值),但是另一个线程已经修改i的值为5了,这个时候正确的值应该是6。

public class VolatileTest {
    public static volatile int i = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new VolatileRun1());
        Thread thread2 = new Thread(new VolatileRun2());
        thread2.start();
        thread1.start();
        thread2.join();
        System.out.println(i);//2   预期值应该是6
    }
    static class VolatileRun1 implements Runnable {
        @Override
        public void run() {
            i = 5;
        }
    }
    static class VolatileRun2 implements Runnable {
        @Override
        public void run() {
            //i++;
            //模仿i++
            int a = i;
            SleepUtils.second(2);
            i = a + 1;
        }
    }
}

volatile建立happens-before规则:

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1; // 1
        flag = true; // 2 
    }

    public void reader() {
        if (flag) { // 3 
            int i = a; // 4
        }
    }
}

假设A、B两个线程分别执行writer和reader两个方法:
1)根据程序次序规则,1 happens-before 2;3 happens-before 4。
2)根据volatile规则,2 happens-before 3。
3)根据happens-before的传递性规则,1 happens-before 4。

2、synchronized关键字
java并发编程中synchronized一定是元老级的锁,之前很多人提到synchronized想到的都是重量级锁,但是1.6版本中加入了偏向锁和轻量级锁,降低了之前锁的获取和释放带来的性能损耗。

偏向锁: 当一个线程获取锁的时候会在同步块所在的对象头中存储一个偏向线程ID,这个线程再次进入的时候只需要简单的对比一下存储的线程ID是否是当前线程ID。如果是,则当前线程获取锁成功,否则,使用CAS来获取锁更新存储的线程ID。
偏向锁的撤销: 偏向锁的释放是一种当竞争关系出现时才会释放的机制,当其他线程竞争时才会释放锁。释放时,会检查持有偏向锁的线程是否存活,如果不处于存活状态则修改为无锁状态,如果持有偏向锁的线程存活则执行完。
轻量级锁 当线程在进入同步快之前会在自己的帧栈中创建一份锁记录的空间来存储对象头的锁信息(复制的),然后CAS操作将对象头的信息修改为指向线程锁记录的指针,如果CAS成功则当前线程获取锁,如果失败,则说明其他线程竞争当前锁,则当前线程最少经过一次自旋获取锁(就是自己检查自己有没有获取锁)。
轻量级锁解锁 轻量级锁解锁时会使用CAS操作将对象头的锁信息(指针)替换回之前复制的存储在自己帧栈中的锁信息,如果成功,则表示没有发生竞争,如果失败则锁升级为重量级锁。
下面是各种锁的优缺点对比:
在这里插入图片描述
总结一下就是:同步快的执行速度快慢决定了优先使用那种锁
同步快执行很快(竞争不多)–>轻量级锁
同步快执行很慢 (竞争很多)–>重量级锁

synchronized关键字的使用
不同于volatile关键字只能使用在字段上,synchronized关键字可以修饰方法和代码块当然字段也可以。
1)·对于普通同步方法,锁是当前实例对象。
2) ·对于静态同步方法,锁是当前类的Class对象。
3) ·对于同步方法块,锁是Synchonized括号里配置的对象。

public class SynchonizedTest {

    public synchronized void test1() {
        for (int i = 0; i < 500; i++)
            System.out.println("test1 加了synchronized的普通方法.。。。。。。。" + Thread.currentThread().getName());
    }

    public synchronized void test01() {
        for (int i = 0; i < 500; i++)
            System.out.println("test01 加了synchronized的普通方法");
    }

    public void test2() {
        for (int i = 0; i < 500; i++)
            System.out.println("test2 不加synchronized的普通方法");
    }

    public static synchronized void test3() {
        for (int i = 0; i < 500; i++)
            System.out.println("test3 加了synchronized的静态方法 ");
    }

    public static synchronized void test4() {
        for (int i = 0; i < 500; i++)
            System.out.println("test4 加了synchronized的静态方法 ");
    }

    public void test5() {
        System.out.println("锁当前实例对象同步代码块执行开始");
        synchronized (this) {
            for (int i = 0; i < 500; i++)
                System.out.println("test5 当前对象实例锁");
        }
    }

    public static Integer i = 1;

    public void test6() {
        System.out.println("锁静态变量同步代码块开始执行");
        synchronized (i) {
            for (int i = 0; i < 500; i++)
                System.out.println("test6 锁静态变量" + Thread.currentThread().getName());
        }
    }

    public Integer j = 1;

    public void test7() {
        System.out.println("锁普通变量同步代码块开始执行");
        synchronized (j) {
            for (int i = 0; i < 500; i++)
                System.out.println("test6 锁普通变量");
        }
    }


    public static void t1() {
        //1、测试同步实例方法 test1 加了synchronized的普通方法 test2 不加synchronized的普通方法
        SynchonizedTest test1 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test1();
                System.out.println();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test2();
            }
        }).start();
        //输出:test1和test2交替输出
        //结论:同一个对象中加了synchronized的普通方法并不影响不加的普通方法
        //test1和test2互不影响
    }

    public static void t2() {
        SynchonizedTest test1 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test5();
                // test5 当前对象实例锁
            }
        }).start();
        //输出依次输出test1和test5
        //结论:锁当前对象和普通方法上锁 锁的对象都是当前实例
    }

    public static void t3() {
        SynchonizedTest test1 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test01();
            }
        }).start();
        //输出:test1和test01依次输出
        //结论:对于两个不同的普通方法加了synchronized 锁的对象是当前实例
    }

    public static void t4() {
        SynchonizedTest test1 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test7();
            }
        }).start();
        //输出test1和test6交替输出
        //结论:普通方法加锁锁的是当前对象 普通成员变量加锁锁的是成员变量synchronized修饰的和实例对象无关

    }

    public static void t5() {
        SynchonizedTest test1 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test4();
            }
        }).start();
        //输出test1和test4交替输出
        //结论:静态变量加锁和同步方法加锁不会影响,两个是独立的
    }

    public static void t6() {
        SynchonizedTest test1 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test6();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test4();
            }
        }).start();
        //输出:test4和test6交替输出
        //静态方法加锁和静态变量加锁两个互不影响,静态方法加锁锁的是Class对象,静态变量加锁锁的是synchronized修饰的变量
    }

    public static void t7() {
        SynchonizedTest test1 = new SynchonizedTest();
        SynchonizedTest test2 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test6();
            }
        }, "test1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.test6();
            }
        }, "test2....").start();
        //输出:依次输出test1线程和test2线程
        //静态方法加锁锁的是Class对象,不同对象实例依然属于同一个Calss对象
    }

    public static void t8() {
        SynchonizedTest test1 = new SynchonizedTest();
        SynchonizedTest test2 = new SynchonizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test1.test1();
            }
        }, "test1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test2.test1();
            }
        }, "test2....").start();
        //输出:交替输出test1线程和test2线程
        //普通方法锁的是当前对象实例,当新创建两个对象实例时两个并不影响
    }

以上代码建议拷贝到自己idea中跑一跑,总结一点对于synchronized关键字的使用就是尽量使用同步代码块的方式。

volatile关键字和synchronized关键字对比

1、 volatile关键字是线程同步的轻量级实现,所以volatile性能肯定要比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized可以修饰方法以及代码块。synchronized关键字在Java1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引起的偏向锁和轻量级锁以及其他各种优化之后执行效率由了显著提升,实际开发中使用synchronized关键字的场景还是更多一些。

2、多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。

3、volatile可以保证数据的可见性,但不能保证数据的原子性,synchronized都能保证。

4、volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。

缺点:这两个关键字并不是使用得越多越好,多线程并发编程一定是比单线程慢的,当过多的使用volatile关键字时会引起频繁的刷新共享内存造成性能下降,同样的synchronized关键字会引起阻塞同样会降低性能,并且阻塞不可被中断、隐式的释放锁(不受我们控制)。所以两个关键字的使用场景一定是在保证线程安全的情况下使用,单线程环境当然不需要。

线程优先级
在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线 程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操 作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较 低的优先级,确保处理器不会被独占。
注意:很多操作系统并不会响应设置的优先级,针对不同的线程类型(cpu密集、io密集的)最好设置不同的线程池(设置不同的线程数量)进行处理。

Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才 从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时 时间里没有终止,那么将会从该超时方法中返回。

线程间的通信

/**
 *  **经典范式
 *   synchronized(对象) {
 *     while(条件不满足) {
 *     对象.wait();
 *   }对应的处理逻辑 }
 *
 *   synchronized(对象) {
 *      改变条件
 *      对象.notifyAll();
 *      }**
 */
public class WaitNotify {
    private static Object lock = new Object();

    private static Boolean flag = true;

    public static void main(String[] args) {
        Thread waitThread = new Thread(new WaitRuner(), "wait");
        waitThread.start();
        Thread notifyThread = new Thread(new NotifyRuner(), "notify");
        notifyThread.start();
    }

    static class WaitRuner implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                while (flag) {
                    try {
                        System.out.println(System.currentTimeMillis() + Thread.currentThread().getName());
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("第二次唤醒");
            }
        }
    }

    static class NotifyRuner implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(System.currentTimeMillis() + Thread.currentThread().getName());
                lock.notify();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = false;
            }
        }
    }
}

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而 执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的 关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

通知等待经典范式:

  • synchronized(对象) {
  • while(条件不满足) {
  •    对象.wait(); }
    
  • 对应的处理逻辑
  • }
    synchronized(对象) {
    改变条件
  •    对象.notifyAll();
    
  • }

安全地终止线程

public class Shutdown {
    public static void main(String[] args) throws InterruptedException {
        //创建两个线程
        Runer one = new Runer();
        Thread oneThred = new Thread(one, "one");
        oneThred.start();
        SleepUtils.second(1);
        oneThred.interrupt();
        Runer two = new Runer();
        Thread twoThred = new Thread(two, "two");
        twoThred.start();
        SleepUtils.second(1);
        two.chanl();
    }

    static class Runer implements Runnable {
        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println(i + Thread.currentThread().getName());
        }

        public void chanl() {
            on = false;
        }
    }
}

示例在执行过程中,main线程通过中断操作(interrupt)和cancel()方法均可使CountThread得以终止。 这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地 将线程停止,因此这种终止线程的做法显得更加安全和优雅。

原创文章 3 获赞 7 访问量 1056

猜你喜欢

转载自blog.csdn.net/qq_30765051/article/details/106101524