多线程 2
线程的生命周期
线程的5种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其 子类的对象被声明并创建时,新生的线程对象处于新建状态;
- 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源;
- 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定 义了线程的操作和功能;
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
- 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
线程状态转换图:=-
线程的同步
线程安全
什么是线程安全?
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。多线程是同时并发,并行执行多行代码指令,要同时考虑所有的情况都符合预期,才能说线程是安全的。
为什么会有线程安全问题?
- 多个线程执行的不确定性引起执行结果的不稳定;
- 多个线程对数据的共享(对同一个变量进行读写),会造成操作的不完整性,会破坏数据。
线程不安全的原因
原子性
原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。表达出一组操作具有不可拆分的特性。
结合例子理解:
X=10;//语句 1
X=Y;// 语句 2
X++;// 语句 3
X=Y+1;//语句4
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中,所以是有原子性的;
语句2实际上包含2个操作,它先要去读取Y的值,再将Y的值写入工作内存,虽然读取的值以及将Y的值写入工作内存这2个操作都是原子性操作,但是合起来就不是原子性操作了;
语句3和语句4,X++和 X= Y+1包括3个操作:读取X的值,进行加1操作,写入新的值,所以也没有原子性;
如果我们不保证原子性,那么当一个线程正在对一个变量操作,中途其他线程插入进来了,这个操作被打断了,结果就可能是错误的。
可见性
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
java线程内存模型:
结合例子理解:
//线程1:
int i= 0;
i= 10;
//线程2:
j=i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把 i 的初始值加载到CPU1的工作内存中,然后赋值为10,那么在CPU1的工作内存当中i的值变为10了,却没有立即写入到主存当中。
然后此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的工作内存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。
这就是没有满足可见性,因为此时线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
说明:
1.对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
2. 同时通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
顺序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
代码重排序: 处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
但是,在多线程中,如果遇到代码重排序会出现这样一个问题:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
比如上面的代码,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,代码重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
线程安全问题的解决方法
我们在前面的关于窗口售票的两个例子中,虽然实现了多线程的创建,但是在运行结果的时候,我们会发现出下了下面两个问题:
问题1:三个窗口都出现了票号为100的票~~
问题2:有一个窗口出现了票号为 -1 的票~~
问题原因:
当某个线程操作出票的过程时,该线程尚未完成操作,其他线程也加入进来,对出票进行操作- - ->这就牵扯到了线程的安全问题~~
解决方法:
当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
同步机制
在JAVA中,我们通过同步机制来解决线程安全问题~~
同步代码块
模板:
synchronized(同步监视器){
//需要被同步的代码块
}
说明:
- 操作共享数据的代码,即为需要被同步的代码(这儿需要注意一定要找准确~~);
- 共享数据:多个线程共同操作的变量。比如: ticket就是共享数据;
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁,但是要求多个线程必须共用同一把锁。
使用同步代码块解决“实现Runnable接口”的线程安全问题
参考代码:
public class ThreadTest_6 {
public static void main(String[] args) {
MyThread_6 window = new MyThread_6();
Thread window1 = new Thread(window);
Thread window2 = new Thread(window);
Thread window3 = new Thread(window);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class MyThread_6 implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){ //可以直接使用synchronized (this){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:"+ ticket);
ticket--;
}else{
break;
}
}
}
}
}
运行结果:
我们可以为了使代码变得简洁,不要额外创建obj对象,而是直接使用this (此处那个对象调用该实例方法,this就是指谁)来充当同步监视器,用自身对象来代替obj。
使用同步代码块解决“继承Thread类”的线程安全问题
参考代码:
public class ThreadTest_4 {
public static void main(String[] args) {
MyThread_4 window1 = new MyThread_4();
MyThread_4 window2 = new MyThread_4();
MyThread_4 window3 = new MyThread_4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class MyThread_4 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run(){
while(true){
synchronized (obj){ //可以换成synchronized (ThreadTest_4.class){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:"+ ticket);
ticket--;
}else{
break;
}
}
}
}
}
运行结果:
这里我们会发现,在创建Object对象时,我们使用了static来进行修饰了,目的还是为了保证多个线程之间是共用的同一把锁,如果不使用static修饰,那么三个窗口会创建三个objec对象。
与此同时,我们为了使代码简洁,可以使用当前类来充当同步监视器,也就是用ThreadTest_4.class来代替obj,这就体现出了“类也是对象”~~
总结
同步代码块方法解决了线程安全问题,但与此同时,在同步代码块内部,只能有一个线程执行,其余线程需等待其中线程执行完成后再进去,这就使得代码执行效率降低了!
同步方法
使用同步方法解决“实现Runnable接口”的线程安全问题
参考代码:
public class ThreadTest_6 {
public static void main(String[] args) {
MyThread_6 window = new MyThread_6();
Thread window1 = new Thread(window);
Thread window2 = new Thread(window);
Thread window3 = new Thread(window);
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class MyThread_6 implements Runnable{ //这儿的同步监视器就是 this
private int ticket = 100;
@Override
public void run() {
while(true){
test();
}
}
public synchronized void test() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
运行结果:
使用同步方法解决“继承Thread类”的线程安全问题
参考代码:
public class ThreadTest_4 {
public static void main(String[] args) {
MyThread_4 window1 = new MyThread_4();
MyThread_4 window2 = new MyThread_4();
MyThread_4 window3 = new MyThread_4();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
class MyThread_4 extends Thread{
private static int ticket = 100;
@Override
public void run(){
while(true){
test();
}
}
private static synchronized void test(){
//这儿的同步监视器就是ThreadTest_4.class
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ ":卖票,票号为:"+ ticket);
ticket--;
}
}
}
运行结果:
总结
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是: this。静态的同步方法,同步监视器是:当前类本身!