java并发与多线程(二):线程安全

线程可以拥有自己的操作栈、程序计数器、局部变量表等资源,它与同一进程内的其他线程共享该进程的所有资源。线程在生命周期内存在多种状态。有NEW(新建状态)、RUNNABLE(就绪状态)、RUNNING(运行状态)、BLOCKED(阻塞状态)、DEAD(终止状态)五中状态。


image.png

(1) New即新建状态

是线程被创建且未启动的状态。创建线程的方式有三种:第一种是继承Thread类,第二种是实现Runnable接口,第三种是实现Callable接口。相比第一种,推荐第二种方式,因为继承自Thread类往往不符合里氏代换原则,而实现Runnable接口可以使编程更加灵活,对外暴露的细节比较少,让使用者专注于实现线程的run()方法上。第三种call()声明如下:


image.png

由此可知,Callable与Runnable有两点不同:第一,可以通过call()获得返回值。前两种方式都有一个共同的缺陷,即在任务执行完成后,无法直接获取执行结果,需要借助共享变量等获取,而Callable和Future则很好地解决了这个问题;第二,call()可以抛出异常。而Runnable只有通过setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕捉到子线程异常。

(2)RUNNABLE,即就绪状态

是调用start()之后运行之前的状态。线程的start()不能被多次调用,否则会抛出IllegalStateException异常。

(3)RUNNING,即运行状态

是run()正在执行时线程的状态。线程可能会由于某些因素而退出RUNNING,如时间、异常、锁、调度等。

(4)BLOCKED,即阻塞状态

进入此状态,有以下情况。
同步阻塞:锁被其他线程占用
主动阻塞:调用Thread()的某些方法,主动让出CPU执行权,比如sleep()、join()等。
等待阻塞:执行了wait()。

(5)DEAD,即终止状态

是run()执行结束,或因异常退出后的状态,此状态不能逆转。

(顺手插一个自己的记录:start()和run()方法的区别:
1、start方法用来启动相应的线程;
2、run方法只是thread的一个普通方法,在主线程里执行;
3、需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法;
4、run方法必去是public的访问权限,返回类型为void。)

线程安全问题只在多线程环境下才出现,单线程串行执行不存在此问题。保证并发场景下的线程安全,可以从以下四个维度考量:

(1)数据单线程可见。

单线程总是安全的。通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。最典型的就是线程局部变量,它存储在独立虚拟机栈帧变量表中,与其他线程毫无瓜葛。ThreadLocal就是采用这种方式来实现线程安全的。

(2)只读对象。

只读对象总是安全的。他的特性是允许复制、拒绝写入。最典型的只读对象有String、Integer等。一个对象想要拒绝任何写入,必须要满足以下条件:使用final关键字修饰类,避免被继承;使用private final关键字避免属性被中途修改;没有任何更新方法;返回值不能可变对象为引用。

(3)线程安全类。

某些线程安全类的内部有非常明确的线程安全机制。比如Stringbuffer就是一个线程安全类,它采用synchronized关键字来修饰相关方法。

(4)同步与锁机制。

如果想要对某个对象进行并发更新操作,但又不属于上述三类,需要开发工程师在代码中实现安全的同步机制。虽然这个机制支持的并发场景很有价值,但非常复杂且容易出现问题。

线程的安全核心理念就是“要么只读,要么加锁”。合理利用好JDK提供的并发包,往往能化腐朽为神奇。Java并发包(java.util.concurrent, JUC)中大多数类注释都写有:@author Doug Lea。如果说Java是一本史书,那么Doug Lea绝对是开疆拓土的伟大人物。Doug Lea在大学当老师时,专攻并发编程和并发数据结构设计,主导设计了JUC并发包,提高了Java并发编程的易用性,大大推进了Java的商用进程。并发包主要分成以下几个类族:

(1)线程同步类。

这些类使线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用Object的wait()和notify()进行同步的方式。主要代表为CountDownLatch、Semaphore、CyclicBarrier等。

(2)并发集合类。

集合并发操作的要求是执行速度快,提取数据准。最著名的类非ConcurrentHas和Map莫属,它不断地优化,由刚开始的锁分段到后来的CAS,不断地提升并发性能。其他还有ConcurrentSkipListMap、CopyOnWriterArrayList、BlockingQueue等。

(3)线程管理类。

虽然Thread和ThreadLocal在JDK1.0就已经引入,但是真正把Thread发扬光大的是线程池。根据实际场景的需要,提供了多重创建线程池的快捷方式,如使用Executors静态工厂或者使用ThreadPoolExecutor等。另外,通过ScheduledExectorService来执行定时任务。

(4)锁相关类。

锁以Lock接口为核心,派生出一些在实际场景中进行互在斥操作的锁相关类。最有名的是ReentrantLock。锁的很多概念在弱化,是因为锁的实现在各种场景中已经通过类库封装进去了。

并发包中的类族有很多,差异比较微妙,开发工程师需要有很好的Java基础、逻辑思维能力,还需要有一定的数据结构基础,才能够彻底分清各个类族的优点、缺点及差异点。
解决线程安全问题的能力时开发工程师进阶的重要能力之一。由于初创公司的业务流量通常比较小,再加上其初级程序员缺乏线程安全意识。所以,即使出现了由高并发导致的错误,往往也由于复现难度大、追踪困难而不了了之。但是在后期的系统重构中,这些公司一定会为以上线程安全隐患买单。

你可能感兴趣的:(java并发与多线程(二):线程安全)