Java的多线程,不论是在工作中还是在面试中都至关重要,对线程的掌握是必须的。
在阅读本文前需要有Java基础知识以及对线程的使用有一定的经验。
1.线程的创建方式
通常有实现Runable和继承Thread类可以实现,但是这两种没有返回值,利用Callable和futuretask可以实现获取线程的返回值。
class TestMain {
public static void main(String[] args) {
Callable<Integer> stringCallable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("call()当前线程的名称为:"+Thread.currentThread().getName());;
int i = 0;
for (int x = 0; x < 10; x++) {
Thread.sleep(4000);
i = i + 5;
}
return i;
}
};
FutureTask<Integer> ft = new FutureTask<>(stringCallable);
try {
new Thread(ft).start();
//这里要注意 get是阻塞当前线程的
System.out.println("当前callable返回的值为"+ft.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.多线程的同步
如果多线程都会对一个变量进行读写操作,这时就会涉及到同步的问题,保证线程同步就是保证操作的原子性,可见性以及有序性。
原子性:指操作的不可分割性,一个操作要么不间断地全部被执行,要么一个也没有执行。
可见性:当变量被修改后,其他线程读取到的值一定是被修改后的值。
有序性:即程序执行的顺序按照代码的先后顺序执行
为何会有同一个变量在不同的线程中表现出不同值?这就涉及到了内存模型
简单的说是因为每个线程对变量进行操作的时候会克隆一个值到自己的工作内存中,操作完成后才会写回主内存中,如果在还没写回数据钱其他线程访问了就会造成数据不一致,从而导致了线程的不同步。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。原子性,有序性,可见性就是保证这个操作的正确。
在java中Synchronized关键词可保证 有序性,可见性,原子性
Volatile关键词保证了操作的有序性以及可见性,AtomicLong以及AtomicBoolean等保证了操作的原子性
下一节将讨论各个关键词的具体使用场景,以及各个场景下的作用