创建线程的方式及实现
一、继承Thread类创建线程类
1、定义Thread的子类,并重写run方法,因为该方法的方法体就是代表了线程要完成的任务,因此run方法又叫做执行体。
2、创建Thread子类的实例,即创建了线程对象。
3、调用线程对象的start方法来启动该线程。
二、通过Runnable接口来创建线程类
1、定义Runnable接口的实现类,并重写run方法,跟Thread一样,该run方法又叫做执行体。
2、创建实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的对象。
3、调用线程对象的start方法来启动该线程。
三、通过Callable和Future创建线程
1、创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
2、创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3、使用FutureTask对象作为Thread对象的target创建并启动新线程。
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
创建线程的三种方式的对比
采用实现Runnable、Callable接口的方式创见多线程时
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
sleep()、join()、yield()区别
1、sleep()
在指定毫秒数为单位,让当前正在执行的线程处于休眠状态,但此操作并不会释放对象锁。
2、yield()
它与sleep类型,也不会释放锁,但它没有参数,即yield()方法只是使当前线程重新回到可执行状态。该方法只能使同优先级或高优先级的线程得到执行机会,这也和sleep方法不同。
3、join()
Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。在A执行完毕之前,B不能工作。
sleep()和wait()的区别
1、对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的
2、sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态
3、在调用sleep()方法的过程中,线程不会释放对象锁
4、而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
CountDownLatch 原理
CountDownLatch是同步工具类之一,可以指定一个计数器,在并发环境下由线程进行减1操作,当计数器变为0时,被await方法阻塞的线程将会被唤醒,实现线程间的同步。
CyclicBarrier 原理
字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时候,屏障才会开门。所有被屏障拦截的线程才会运行。
说说Semaphore原理
信号量,用来控制多线程同时访问特定资源时的线程数量,它通过协调合理的使用公共资源。线程通过acquire方法活动信号量的许可,当信号量没有可用的许可的时候,线程阻塞,直到有可用的线程为止。线程可以通过releace方法来释放它持有的信号量许可。
说说 Exchanger 原理
JDK1.5提供的工具套件,用来处理两个线程之间的数据交换。
ThreadLocal 原理分析
线程局部变量,并发的时候成员变量是不安全的,因为同时有多个线程在访问,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本,当我们调用get方法的时候,其实每个当前线程中都有一个ThreadLocal。每次获取或者设置都是对该ThreadLocal进行的操作,是与其他线程分开的。
线程池的几种方式
1、newFixedThreadPool(int nThreads):创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程的规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
2、newCachedThreadPool():创建一个可缓存的线程池,如果线程池超过了处理需求,将自动回收线程池,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制
3、newSingleThreadExecutor():单线程的Executor,它创建单个工作线程来执行任务,如果这个任务异常结束,会创建一个新的来替代它,它的特点是能确保依照任务在队列中的顺序来串行执行
4、newScheduledThreadPool(int corePoolSize):创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程的生命周期
新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blockde)、死亡(Dead)
新建(new Thread):当创建Thread类的实例对象时,此线程进入新建状态(未被启动)
就绪(Runnable):线程被启动,正在序列队列中等待被分配给CPU资源
运行(Running):线程获得了CPU资源,正在执行任务(run()方法),运行中除非该线程自动放弃CPU资源或有更高优先级的线程进入,否则它将一直执行下去到任务结束。
死亡(dead):该线程执行完毕后或被其他线程杀死,线程就进入到了死亡状态,此时线程不可能再进入等待执行状态
阻塞(Blocked):由于某些原因导致该线程让出CPU并暂停自己的执行。
锁机制
说说线程安全问题
线程安全是指要控制多个线程对某个资源的有序访问或修改,而在这些线程之间没有产生冲突。
线程安全一般体现在两个方面:
1、多个Thread对同一个实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized上。如ArrayList和Vector,HashMap和HashTable(后者每个方法前都有synchronized关键字)
2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有java关键字支持,如static、transient那样。
悲观锁 乐观锁
悲观锁:总是假设最坏的情况,每次取数据时都认为其他线程会修改,在Java中,synchronized的思想也是悲观锁。
乐观锁:总是认为不会产生并发问题,每次取数据的时候总是认为不会被其他线程修改,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现
1、version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
2、CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。