深入理解Java多线程(二)

关于java多线程的概念以及基本用法:java多线程基础

2,多线程的同步

多个线程对同一对象的变量进行同时访问时会引发线程的安全问题,即一个线程对一个变量修改后,其他线程可能会读取到修改后的变量值,所以我们要对获得的实例变量的值进行同步处理,保证其原子性

2.1,多个线程访问多个对象

synchronized关键字取得的锁都是对象锁,意思就是若多个线程访问同一个一个变量,其实这个变量就是一个对象,当一个线程先执行被synchronized关键字修饰的代码时,这个线程拥有这个对象的锁,其他线程只能等待
但是,若多个线程访问多个对象时,JVM此时会产生多个锁,此时可能会产生异步

若一个对象中存在synchronized类型方法和非synchronized类型方法,并且A线程拥有此对象synchronized类型方法的锁,则B线程可以以异步的方式调用对象中非synchronized类型方法,若B想调用synchronized类型方法,则需要等待

2.2,脏读

synchronized关键字作用的都是共享变量,但是synchronized关键字是对变量赋值时进行了同步,但是取值时可能出现一些意想不到的意外,这种意外就是脏读
发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了,解决办法就是将getValue()方法添加synchronized关键字以实现同步,防止其他线程在一个线程执行时访问

2.3,synchronized同步代码块

是不是感觉synchronized关键字很有用,只要加上它代码就可以实现同步了,但是把别忘了,它会让其他线程等待,这样等待的时间是很长的,这也是它的缺点,下面介绍synchronized同步代码块

synchronized(this)同步代码块:一段时间内只能有一个线程执行代码块,其他线程必须等待当前线程执行完这个代码块之后才能执行该代码块

下面来比较一下synchronized(this)同步代码块和synchronized关键字的耗时情况,首先是synchronized关键字
Task类:

public class Task {
    private String getData1;
    private String getData2;
    synchronized public void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);
            getData1 = "threadName1 ="+Thread.currentThread().getName();
            getData2 = "threadName2 ="+Thread.currentThread().getName();
            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

CommonUtils类:

public class CommonUtils {
    public static long beginTime1;
    public static long beginTime2;
    public static long endTime1;
    public static long endTime2;
}

MyThread1类:

public class MyThread1 extends Thread{

    private Task task;

    public MyThread1 (Task task) {
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();
    }

}

MyThread2类:

public class MyThread2 extends Thread{
    private Task task;
    public MyThread2(Task task) {
        super();
        this.task = task;
    }
    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();
    }


}

测试类:

public class Run {

    public static void main(String[] args) {
        Task task = new Task();
        MyThread1 thread1 = new MyThread1(task);
        thread1.start();
        MyThread1 thread2 = new MyThread1(task);
        thread2.start();
        try {
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        long beginTime = CommonUtils.beginTime1;
        if(CommonUtils.beginTime2<CommonUtils.beginTime1) {
            beginTime = CommonUtils.beginTime2;
        }
        long endTime = CommonUtils.endTime1;
        if(CommonUtils.endTime2>CommonUtils.endTime1) {
            endTime = CommonUtils.endTime2;
        }
        System.out.println("耗时:"+((endTime-beginTime)/1000));
    }
}

结果:

begin task
threadName1 =Thread-1
threadName2 =Thread-1
end task
begin task
threadName1 =Thread-0
threadName2 =Thread-0
end task
耗时:6

总用时为6秒,下面将Task类代码修改,测试synchronized(this)同步代码块的时耗
Task类:

    private String getData1;
    private String getData2;
    public void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);
            synchronized(this) {
            getData1 = "threadName1 ="+Thread.currentThread().getName();
            getData2 = "threadName2 ="+Thread.currentThread().getName();
            }
            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

结果:

begin task
begin task
threadName1 =Thread-1
threadName2 =Thread-0
end task
threadName1 =Thread-0
threadName2 =Thread-0
end task
耗时:3

synchronized关键字,synchronized(this)和synchronized(非this类):

为什么synchronized(this)同步代码块耗时会比synchronized关键字的短,这是因为synchronized关键字是对变量或者方法修饰的,而synchronized(this)同步代码块是对一段代码修饰的,一个线程访问synchronized(this)同步代码块修饰的代码块时,其他线程可访问和这个代码块在同一个方法里的未被修饰的代码,从而减少了等待的时间
当一个线程访问synchronized(this)同步代码块时,其他线程对同类中的其他synchronized(this)同步代码块的访问将被阻塞,两个同步代码块指向的是同一个对象即当前的对象(拥有的是同一把锁)
如果使用this对象,一个类中有很多synchronized方法,一个线程访问了synchronized方法,那么其他synchronized方法不能被其他线程访问,造成阻塞,这会影响运行的效率,好在Java支持将“任意对象”作为“对象监视器”来实现同步的功能,这里你的任意变量大多是实例变量以及方法的参数
下面来测试一下将任意对象作为对象监视器能不能实现线程的同步
Service类:

public class Service {
    private String usernameParam;
    private String passwordParam;
    private String anything = new String();

    public void setUsernamePassword(String username,String password) {
        try {
            synchronized (anything) {
                System.out.println(Thread.currentThread().getName()+
                        "在"+System.currentTimeMillis()+"进入同步块");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println(Thread.currentThread().getName()+
                        "在"+System.currentTimeMillis()+"离开同步块");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

线程A:

public class ThreadA extends Thread{
    private Service service;
    public ThreadA(Service service) {
        super();
        this.service = service;
    }

    public void run() {
        service.setUsernamePassword("a", "aa");
    }
}

线程B:

public class ThreadB extends Thread{
    private Service service;
    public ThreadB(Service service) {
        super();
        this.service = service;
    }
    public void run() {
        service.setUsernamePassword("b", "bb");
    }
}

测试类:

public class Run {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();
    }
}

结果:

A在1534148762805进入同步块
A在1534148765805离开同步块
B在1534148765805进入同步块
B在1534148768805离开同步块

显然结果是同步的,
这是因为对象监视器使用的是同一个对象service,若使用不同对象,则会产生异步的现象
保持代码不变,改变Service类
改变后的Service类:

public class Service {
    private String usernameParam;
    private String passwordParam;
    //private String anything = new String();

    public void setUsernamePassword(String username,String password) {
        try {
            String anything = new String();
            synchronized (anything) {
                System.out.println(Thread.currentThread().getName()+
                        "在"+System.currentTimeMillis()+"进入同步块");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println(Thread.currentThread().getName()+
                        "在"+System.currentTimeMillis()+"离开同步块");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果:

A在1534148933716进入同步块
B在1534148933716进入同步块
B在1534148936717离开同步块
A在1534148936717离开同步块

synchronized(非this对象x)格式的写法是将x对象本身作为“对象监视器”,这样就可以得出以下3个结论:
1,当多个线程同时执行synchronized(X){}同步代码块是呈同步效果
2,当其他线程执行x对象中synchronized同步方法时呈现同步效果
3,当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果
4,当其他线程调用不加synchronized关键字的方法时,还是异步调用

2.4,多线程死锁

产生死锁的必要条件:

  • 互斥条件 :指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用
  • 请求和保持条件:指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又已经被其它进程占用。请求阻塞,但又对自己已经获得的资源保持不放
  • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能够被剥夺,只能在使用完时由自己释放。即:非可剥夺性资源
  • 环路等待条件:在死锁发生时,存在一个进程---资源的环形链,即进程集合{P0,P1,P2,…,Pn)的进程形成一个相互连环

新建DealThread类:

public class DealThread implements Runnable{
    public String username;
    public Object lock1 = new Object();
    public Object lock2 = new Object();
    public void setFlag(String username) {
        this.username = username;
    }

    @Override
    public void run() {
        if(username.equals("a")) {
            synchronized(lock1) {
                try {
                    System.out.println("username="+username);
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("按lock1->lock2代码顺序执行了");
                }
            }
        }
        if(username.equals("b")) {
            synchronized(lock2) {
                try {
                    System.out.println("username="+username);
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("按lock2->lock1代码顺序执行了");
                }
            }
        }
    }
}

测试类:

public class Run {
    public static void main(String[] args) {
        try {
            DealThread dt1 = new DealThread();
            dt1.setFlag("a");
            Thread thread1 = new Thread(dt1);
            thread1.start();
            Thread.sleep(100);
            dt1.setFlag("b");
            Thread thread2 = new Thread(dt1);
            thread2.start();

        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

结果:

username=a
username=b

两个线程都执行到了System.out.println(“username=”+username)(互斥条件);然后开始互相等待对方的对象锁,但是都对自己现有的资源不放手(请求和保持条件,环路等待条件),两个线程都未执行完,所以系统不会回收资源(不剥夺条件),所以产生了死锁

2.5,volatile关键字

volatile关键字关键字的主要作用是强制从公共堆栈中取得变量的值,使变量在多个线程间可见,并不是从线程私有数据堆栈中取得变量的值,这里其实涉及到计算机内存的知识
计算机执行程序都是在CPU中进行的,存储数据是在物理内存中,每次执行的时候就要把物理内存的数据读取到CPU里,CPU执行完再把数据写回物理内存,这个I/O操作相对CPU计算速度是很慢的,所以在CPU设置了高速缓存,不是很大但是很快,执行程序时,将相关数据一次放到高速缓存中,CPU执行完后再刷新物理内存数据,这样计算效率就大大提高了
但是这也会引发一个问题,会有一段时间物理内存的数据和高速缓存的数据不一样,为了解决缓存不一致性问题,一般采用通过在总线加LOCK#锁的方式和缓存一致性协议,这些都是硬件层面的解决办法
下面来看看如何用volatile关键字解决死循环
新建RunThread类:

public class RunThread extends Thread{
    private boolean isRunning = true;
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public void run() {
        System.out.println("进入run了");
        while(isRunning == true) {
        }
        System.out.println("线程停止了");
    }
}

测试类:

public class Run {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
            System.out.println("已经复制为false");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果:

进入run了
已经复制为false

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。上面的例子里,private boolean isRunning = true;存在于主存和工作内存中,但是thread.setRunning(false);更新的是主存中isRunning的值,所以程序一直是死循环

将RunThread类代码做更改,其余代码不变:

volatile private boolean isRunning = true;

结果:

进入run了
已经复制为false
线程停止了

使用volatile关键字修饰后,当线程访问isRunning这个变量时,强制从物理内存中读取变量的值,是不是觉得很好,但是好的东西还是有缺点的,volatile关键字缺点就是不具备同步性,也就不具备原子性

猜你喜欢

转载自blog.csdn.net/qq_37438740/article/details/81603812