- 在java中,除去long型和double型以外的任何类型的变量的写操作都是原子操作。但是java规范还特别规定对于volatile关键字修饰的long/double型变量的写操作具有原子性。
- 上下文切换在某种程度上可以被看作多个线程共享同一个处理器的产物。
Java实现多线程的三种方式
- 继承Thread类,并重写run方法。
- 实现Runnable接口,并重写run方法。
- 实现Callable接口,并重写run方法,并使用FutureTask包装器。
竞态的模式与竞态产生的条件
两种模式:(read-modify-write)读-改-写 、 (check-then-act)检测而后行动
可能会导致竞态产生的条件
- 状态变量:即类的实例变量,静态变量
- 共享变量:可以被多个线程共同访问的变量。
注:局部变量不会导致竞态,也可以使用synchronized关键字来保证某一时刻只能被一个线程执行
安全问题表现为:原子性 可见性 有序性
- 对于上面静态产生的条件,我们可以将操作封装为原子操作,即可避免该问题。例如局部参数或者synchronized
- 如果一个线程对某个共享变量进行更新之后,后续访问该变量的线程可以读取到更新的结果,那么我们就称这个线程对该共享变量的更新是可见的。处理器并不是直接与主内存打交道而执行内存的读写,而是通过寄存器,高速缓存,写缓冲器和无效化队列等部件执行内存的读写操作。java中可以使用volatile来解决该问题。
- volatile和synchronized关键字都可以实现有序。
线程同步机制
- 按照java虚拟机对锁的实现方式划分,java平台中的锁包括内部锁和显示锁。内部锁通过synchronized关键字实现;显示锁是通过java.concurrent.locks.Lock接口的实现类(如 java.concurrent.locks.ReentrantLock类)实现的。
锁的基本概念
- 可重入行
一个线程在其持有一个锁的时候能否再次(或者多次)申请该锁,如果可以再次申请成功,我们则称该锁为可重入的。(实现:我们可以理解可重入锁为一个对象,该对象包含了一个计数器属性,初始值为1,表示相应的锁还没有被任何线程持有。每次线程获得一个可重入锁的时候,值加1。)
常见问题
- wait,notify 和 notifyAll 是在 Object 类中定义的而不是在 Thread 类中定义
-
wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用, 那么 Object 类则是的正确声明位置。
-
每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因。
-
在 Java 中为了进入代码的临界区,线程需要锁定并等待锁定,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且他们应该等待取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁定。
-
Java 是基于 Hoare 的监视器的思想。在Java中,所有对象都有一个监视器。
线程在监视器上等待,为执行等待,我们需要2个参数:一个线程,一个监视器(任何对象)在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中侵入的操作都被弃用了(例如 stop 方法)。 -
IO密集型线程数一般是2n+1;cpu密集型线程数一般是n+1
Runnable接口和Callable接口的区别
- Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已。
- Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable + Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。