Java多线程编程
来源:互联网 发布:2016年实体店倒闭数据 编辑:程序博客网 时间:2024/06/03 13:20
本文于多个平台发布,仅为与更多人进行交流。
纯干货,如笔记,略枯燥,但求精华。
背景:
多线程开发中,重要的一点关注点,就是线程安全,保障数据的一致性,不会出现脏数据。
而对于多线程,主要有三个特性,原子性、可见性、有序性。
线程内存管理:
- 每一个线程都拥有一份工作内存,是私有的本地内存(local memory),并将共享变量拷贝一份副本存储在私有本地内存中。
- 线程之间通过主内存(main memory)进行资源共享,所有的变量都存储在主内存中。
- 线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量
线程通信:
通过线程之间的通信保持数据的一致性。
消息传递:通过显示的方式进行消息的传递,比如说调用wait(),notify()等方法进行通信。线程之间没有公共的状态,只能通过明确的发送消息来进行通信。
共享内存:线程之间通过读写共享变量来进行隐式的通信。线程与进程(本地内存与公共内存),有8种内存操作:lock/unlock,read/load/use,assign/store/write。
Volatile:
既然每一个线程都有自己的工作内存,会把变量先创建一个副本存储于自己的工作内存内,也就是说,线程并不会实时的去主内存获取变量的值。如果该变量是一个共享变量,这将会导致一系列的问题。比如,该变量是一个开关,那么它将起不了作为一个开关的作用,此时,我们可以使用volatile。
volatile告诉线程该变量是易变的,不稳定的,在需要使用到由该关键词修饰的变量时,都需要从主内存中重新获取一次,也就是要同时执行read-load-use,执行后对volatile变量操作后,要同时执行assign-store-write。保证所修饰变量的可见性,在一个线程中修改变量的值以后,在其他线程中能够看到这个值。
然而volatile仅仅保证了可见性,但并没有保证原子性。也就是一个对volatile变量的操作,并不是马上回写到主内存中的。
synchronized:
synchronized主要针对的是执行顺序,通过synchronized来控制一段代码是否允许并发来达到对执行顺序的控制。同时,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性。
Java原子操作Atomic:
Java自身提供了一些保障原子操作的对象,如上图所示。如我们常用的AtomicInteger,能够保证原子性。它提供了一些自增自减的原子性操作,保障了多线程并发的线程安全。
以上是JAVA多线程涉及一些基础知识,下面我们将通过一个个实例,进一步深入理解多线程。
首先,我们编写一个公共的多线程执行类:
public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService service=Executors.newFixedThreadPool(Integer.MAX_VALUE); final CjtTest ct = new CjtTest(); for (int i = 0; i < 2000; i++) { service.execute(new Runnable() { @Override public void run() { ct.inc(); } }); } service.shutdown(); //确保所有线程都运行完 Thread.sleep(2000); System.out.println("运行结果:Counter.count=" + CjtTest.count); }}
案例一–没有任何保护的多线程任务:
下面我们采用共享内存的方式进行通信:
public class CjtTest { public static int count = 0; public void inc() { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; System.out.println(count); }}
运行结果:
运行结果:Counter.count=1960
解析:
每一个线程在执行时,都会执行一次read-load的原子操作。然而大量线程执行read-load操作时,前面的线程还未执行store-write操作进行回写时,后面的线程已经执行了read-load操作。出现read-read-load-load-store-store-write-write的操作。致使一些自加未正确的统计到count中。
案例二–使用volatile保证可见性
public class CjtTest { public volatile static int count = 0; public void inc() { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; System.out.println(count); }}
运行结果:Counter.count=1960
解析:
Volatile仅仅保证了可见性,也就是说,保证了每一个线程使用该变量时,都会从共享内存中去获取最新的值,但并没有保证原子性,意味着,后面的线程去取这个值时,前面的线程并还没有将执行后的值回写到主内存中。
案例三–使用原子操作对象AtomicInteger
public class CjtTest { public static AtomicInteger count = new AtomicInteger(0); public void inc() { try { Thread.sleep(2); } catch (InterruptedException e) { } count.getAndIncrement(); }}
运行结果:Counter.count=2000
解析:
终于,这里我们获得了正确的数据了。因为AtomicInteger能够保证对象的原子性。即每次只能有一个线程能够对该对象进行操作,且操作前是从主内存获取并且执行完后又立即回写到主内存中。
案例四–使用synchronized锁住非static方法:
public class CjtTest { public static int count = 0; public synchronized void inc() { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; }}
然后我们改写执行类:
每一条线程将new一个对象
public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService service=Executors.newFixedThreadPool(Integer.MAX_VALUE); for (int i = 0; i < 2000; i++) { final CjtTest ct = new CjtTest(); service.execute(new Runnable() { @Override public void run() { ct.inc(); } }); } service.shutdown(); Thread.sleep(2000); System.out.println("运行结果:Counter.count=" + CjtTest.count); }}
运行结果:Counter.count=1956
我们再恢复一下多线程执行类:
public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService service=Executors.newFixedThreadPool(Integer.MAX_VALUE); final CjtTest ct = new CjtTest(); for (int i = 0; i < 2000; i++) { service.execute(new Runnable() { @Override public void run() { ct.inc(); } }); } service.shutdown(); Thread.sleep(6000); System.out.println("运行结果:Counter.count=" + CjtTest.count); }}
运行结果:Counter.count=2000
解析:
首先,我们要正确理解synchronized修饰普通方法时的意义:
如上图,当你使用到如左图methodA时,其实它的真实作用如右图所示。synchronized会锁住自身整个对象。所以在本案例中,我们先采用的是每一条线程new一个新对象,这样,在多线程执行的时候,每一条线程synchronized加锁的都是自己所new的对象,并没有能够起到一个互斥的作用。而在后面,我们修改了一些多线程的代码,所有的线程都共享一个对象,这时候synchronized加锁对象只有一个选择,就能在各个线程中起到了一个互斥的作用。
既然synchronized修饰非static方法时,是锁住this,也就是对象本身,那我们扩展一下,以下methodA和methodB可以同时调用吗?
答案是否定的,因为使用这两个方法的其中一个,都需要先加锁该对象,所以这两个方法是互斥。
案例五–使用synchronized锁住static方法
上一个案例,我们讲解了synchronized修饰非static方法,自然也有人会问,如果用synchronized修饰static方法会怎样?
public class CjtTest { public static int count = 0; public synchronized static void inc() { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; }}
运行结果:Counter.count=2000
解析:
在这个案例,我没有贴出多线程执行类,因为不管你怎么多线程调用(只创建一个对象还是每条线程创建一个对象,都会一样的结果,均可以获得准确的结果。)
这是为什么呢?同样,我们也要清晰理解,这时候synchronized加锁的是什么东西。
synchronized修饰static方法,它相当于锁住的是类。所以不管你创建多少对象,它锁住的是这个class,一样可以起到互斥的作用。
案例六–同一个类中既有synchronized修饰的static方法也有synchronized修饰非static方法:
public class CjtTest { public static int count = 0; public synchronized static void inc() { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; } public synchronized void inc2() { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; }}
修改执行类如下:
public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService service=Executors.newFixedThreadPool(Integer.MAX_VALUE); final CjtTest ct = new CjtTest(); for (int i = 0; i < 2000; i++) { service.execute(new Runnable() { @Override public void run() { ct.inc(); ct.inc2(); } }); } service.shutdown(); Thread.sleep(6000); System.out.println("运行结果:Counter.count=" + CjtTest.count); }}
运行结果:Counter.count=3973
解析:
单独调用inc()或者inc2()方法,都可以得到2000,但每条线程同时调用inc()和inc2(),却得不到4000。
让我们回忆一下,案例4和案例5所描述的,synchronized修饰普通方法时,synchronized锁住的是this,也就是对象。而synchronized修饰static方法时,synchronized锁住的是class。所以同时调用这两个方法,并不能起到一个互斥的作用。
案例七–创建一个lock属性,用synchronized进行加锁
lock为普通属性:
public class CjtTest { public static int count = 0; private Object lock = new Object(); public void inc() { synchronized(lock) { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; } }}
我们采用案例4的多线程执行类:
如果只所有线程只创建一个对象,可以稳定得到2000。
如果每条线程都创建一个自己的对象,那么将得不到2000。
lock为静态属性:
public class CjtTest { public static int count = 0; private static Object lock = new Object(); public void inc() { synchronized(lock) { try { Thread.sleep(2); } catch (InterruptedException e) { } count++; } }}
不管是否共享一个对象还是创建多个对象,依然可以稳定得到2000。
解析:
synchronized修饰对象与修饰方法具有同样的道理。
结语:
希望通过以上一些简单的小例子,能够让你对Java的多线程有了一个初步的了解。
- 【JAVA】JAVA多线程编程
- 【java】:java多线程编程
- Java多线程编程初步
- Java 多线程编程
- Java多线程编程详解
- Java多线程编程经验谈
- Java 多线程编程
- Java多线程编程详解
- Java多线程编程详解
- Java 5.0多线程编程
- Java 5.0多线程编程
- Java 5.0多线程编程
- Java多线程编程详解
- Java多线程编程详解
- Java 5.0多线程编程
- Java多线程编程详解
- java基础教程-多线程编程
- Java多线程编程详解
- Leetcode练习 #4Median of Two Sorted Arrays
- tcp 和 udp 的区别 ,及 udp 实现可靠传输
- Java Map entrySet方法
- SSH与SSM学习之hibernate12——hibernate中的事务
- String优势与StringBuffer,StringBuider优势,还有存于不同区域存在的性能差别
- Java多线程编程
- SecureCRT链接失败
- coocs项目的创建
- Map list set 比较
- 服务器开发---开发环境配置
- MySQL第一天初识--对数据库和表的增删改查
- 解决Selenium2Library 导入报错问题
- 数据库连接池DataSource
- LeetCode 53. Maximum Subarray--Divide and Conquer(分治法)