什么时候数据在多线程并发的环境下会产生不安全问题。
三个条件:
条件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尽量不要嵌套使用。