线程同步进阶

140 阅读7分钟

什么时候数据在多线程并发的环境下会产生不安全问题。

	三个条件:
	条件1:多线程并发
	条件2:有共享数据
	条件3:共享数据有修改的行为

如何解决线程安全问题?

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在前程安全问题
	线程排队执行(不能并发)
	用排队的方式解决线程安全问题
	这种机制被称为:线程同步机制。
	这就是线程同步,实际上就是线程不能并发,线程必须排队执行

关于线程同步这一块,涉及两个专业术语:
		异步编程模型:
			线程t1与线程t2,各自执行各自的,t1不管t2,t2也不管t1
			谁也不需要等谁,这种编程模型叫做:异步编程模型(效率较高)

		同步编程模型:
			t1与t2.在线程t1执行的时候,必须等待t2线程执行结束。或者在t2线程执行的时候,
			必须等待t1线程执行结束。这两个线程之间就发生了等待关系,这就是同步编程模型。

synchronized关键字(内部锁)

		java中每个对象都有一个与之关联的内部锁,这种锁也称为监视器,这种内部锁是一种排他锁,可以保障原子性,可见性与有序性
			内部锁时通过synchronized关键字实现的.synconhronized关键字修饰代码块,修饰该方法
				修饰代码块的语法:
				synchronized(对象锁){
					同步代码块,可以在同步代码块中访问共享数据
				}
			修饰实例方法就称为同步实例方法
			修饰静态方法称为同步静态方法
package Thread;

public class chui {
    public static void main(String[] args) {
        chui c = new chui();

        new Thread(new Runnable() { //使用匿名内部类创建线程thread-0
            @Override
            public void run() {
                c.mm();
            }
        }).start();
        new Thread(new Runnable() {//thread-1
            @Override
            public void run() {
                c.mm();
            }
        }).start();
    }
    public void mm(){
        synchronized (this){//使用锁对象锁住,通常使用this当前对象作为锁对象


            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);

            }
        }
    }
}
	对以上程序的思考
	1.假设Thread-0线程获得CPU执行权
	调用c对象中的mm()方法,执行方法体,先获得this对象c的锁,执行for循环。
	2.而在Thread-0线程还在执行的时候,Thread-1线程是无法获得调用c对象的权利。只能进入阻塞状态。
	3.只有在thread-0线程执行完之后,thread-1线程才能够调用c对象的mm方法。进行for循环
package Thread;

public class big {
    public static void main(String[] args) {
        //创建一个用户对象
        Account  act = new Account("act-01",10000);

        Thread  t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        //设置名字
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class Account {
    //账号
    String atno;
    //余额
    double Blance;
    public Account(){

    }
    public Account(String atno ,double blance){
        this.atno=atno;
        this.Blance=blance;
    }
    public String getAtno() {
        return atno;
    }

    public void setAtno(String atno) {
        this.atno = atno;
    }

    public double getBlance() {
        return Blance;
    }

    public void setBlance(double blance) {
        Blance = blance;
    }
public void withdraw(double money){
        //并发执行该方法
        double before = this.getBlance();
        double after = before - money;
        this.setBlance(after);
}


}

class AccountThread extends  Thread{//创建该类的目的是为了让同一个账户调用withdraw取款方法,而账户只有一个。从而实现并发的情况。
    private Account act ;

    public AccountThread(Account act){//利用构造方法传入Account来实现
        this.act=act;
    }
    @Override
    public void run() {
        //取款操作
        double money =5000;
        act.withdraw(money);
        System.out.println("账户act-01取款成功,余额"+act.getBlance());
    }
}

以上的程序会让程序中的取款操作并发执行的情况下,线程进入取款方法时,可能会同时对存款进行操作,例如线程t1在执行withdrow的第一条语句double before = this.getBlance();时
t2线程还没执行到第三条语句this.setBlance(after);,此时在这一瞬间,用户账户就会被错误地读写。

我们可以在Account中的withdrow方法添加一个锁

package Thread;

public class big {
    public static void main(String[] args) {
        //创建一个用户对象
        Account  act = new Account("act-01",10000);

        Thread  t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        //设置名字
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class Account {
    //账号
    String atno;
    //余额
    double Blance;
    public Account(){

    }
    public Account(String atno ,double blance){
        this.atno=atno;
        this.Blance=blance;
    }
    public String getAtno() {
        return atno;
    }

    public void setAtno(String atno) {
        this.atno = atno;
    }

    public double getBlance() {
        return Blance;
    }

    public void setBlance(double blance) {
        Blance = blance;
    }
public void withdraw(double money) {
    synchronized (this) {
        //并发执行该方法
        double before = this.getBlance();
        double after = before - money;
        this.setBlance(after);
    }
}


}

class AccountThread extends  Thread{
    private Account act ;

    public AccountThread(Account act){
        this.act=act;
    }
    @Override
    public void run() {
        //取款操作
        double money =5000;

        act.withdraw(money);
        System.out.println("账户act-01取款成功,余额"+act.getBlance());
    }
}

我们从以上程序发现我们做的改进仅仅是在withdrow方法中添加了一条语句 : synchronized (this)

而该操作也保证了两个线程调用该方法时的数据安全问题。

synchronized有三种写法

    第一种:同步代码块
		灵活
		synchronized(线程共享对象)
		{
			同步代码块
		}

	第二种:在实例方法上使用synchronized
		表示共享对象一定是this
		并且同步代码块是整个方法体

	第三种:在静态方法上使用synchronized
		表示找类锁
		类锁永远只有1把
		就算创建了一百个对象,那类锁也只有1

数据安全问题对于三大变量

	局部变量:在栈中,每个线程在创建时start方法都会在jvm中创建一个单独的栈空间,每个线程的栈空间都是互相独立的,所以局部变脸不存在线程安全问题
	成员变量:在堆中,堆是唯一的,所以线程都是共享通过一个堆,会存在线程安全问题
	静态变量:在方法区中,方法区也是共享的,也会有线程安全问题

如果要实现线程同步的思考:

	现实开发过程中我们并不是一上来就使用synchronized关键字来保证线程同步。
	synchronized关键字对于性能消耗很大,用户无法得到很好的体验。

	第一种方案:尽量使用局部变量代替实例变量与静态变量。

	第二种方案:如果必须使用实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不会共享了。
	(一个线程对应一个对象,100个线程对于一百个对象,对象不共享,数据就不会出现数据安全问题)

	第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized关键字了。

守护线程:

	线程分为两类:用户线程与守护线程
	守护线程的作用是在其守护的线程运行时一直运行,等到该线程被jvm回收时,守护线程也会自动停止,并且被回收。

定时器:

	间隔特定的时间,执行特定的程序。
	
	在Java实际开发中,每隔多久执行依次程序这样时很常见的,java有多种实现方式:
		可以使用sleep方法。睡眠,设置睡眠时间。

		早java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用,
		不过这种方式在开发中也很少用,因为现在很多高级框架都支持定时任务。

		在实际开发中,目前使用较多的Spring框架中提供的SpringTask框架,
		这个框架只要进行简单的配置,就可以完成定时器的任务。

实现线程的第三种方式:FutureTask方式,实现Callable接口。(jdk 8 新特性)

package Thread_2;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Thread_test01  {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {//相当于run()方法
                System.out.println("call method begin");
                Thread.sleep(1000*10);
                System.out.println("call method end");

                int a = 100;
                int b = 100;
                return  a+b;
            }
        });
        Thread t = new Thread(task);

        t.start();
        //用对象接收返回值,因为int被包装为integer类型
        Object obj = task.get();
        //该方法会阻塞主线程,因为主方法中需要等待get返回的数据,而数据需要止线程执行结束才能得到数据。
    }
}
关于Object类中的wait与notify方法(生产者与消费者模式)
wait方法阻塞线程,notify方法唤醒线程。

生产者与消费者问题

public class test{
	public static void main(String[] args){
		List list = new ArrayList();
		
		Thread producer = new Thread01();
		Thread counster = new Thread02();

		producer.start();
		counster.start();

	}
}
class producer{
	private List list;
	public producer(List list){
	this.list=list;
	}

	while(true){
		synchronized(list){
			if(list>0){
				list.wait();
			}
			Object obj = new Object();
			list.add(obj);
			System.out.println(Thread.currentThread().getName() + "-->" +obj);
			list.notify();
		}
	}
}
class counster{
	private List list;
	public counster(List list){
	this.list=list;
	}

	while(true){
		synchronized(list){
			if(list==0){
				list.wait();
			}
			Object odj = list.remove(0);
			System.out.println(Thread.currentThread().getName() + "-->" +obj);
			list.notify();
		}
	}
}

死锁(deadlock)

public class test{
	public static void main(String[] args){
		Object o1,o2;
		Thread01 t1 = new Thread01(o1,o2);
		Thread02 t2 = new Thread02(o1,o2);
		new Thread(t1,"t1").start();
		new Thread(t2,"t2").start();
}
class Thread01{
	Object o1,o2;
	public class Thread01(Object o1,Object o2){
		this.o1=o1;
		this.o2=o2;
	}
	synchronized(o1){
		sleep(1000);
		synchronized(o2){
		}
	}

}

class Thread02{
	Object o1,o2;
	public class Thread02(Object o1,Object o2){
		this.o1=o1;
		this.o2=o2;
	}
	synchronized(o2){
		sleep(1000);
		synchronized(o1){
		}
	}
}

所以,synchronized尽量不要嵌套使用。