存在线程安全问题的三个条件
1、多线程并发
2、有共享数据(重点)
java中的三大变量:实例变量、静态变量、局部变量
实例变量和静态变量分别存在堆内存和方法区内存,是唯一的,属于共享数据,故线程不安全
而局部变量是在栈内存中,一个栈内存是一个线程,局部变量永远不会共享,所以是线程安全的。
3、共享数据有修改行为
解决线程安全问题的方法:线程同步机制(关键字:synchronized)
线程同步的是让线程排队执行,实际就是线程不能再并发了。线程同步会牺牲一定的效率。
案例:两个人向同一个账户同时取钱,假设账户为10000元,两个人各取5000,如下列代码:
package 多线程;
public class 两个线程对同一账户取款 {
public static void main(String[] args) {
Account act=new Account("act001",10000);//初始化银行账户,拥有10000存款
Thread t1=new AccountThread(act);t1.start();
Thread t2=new AccountThread(act);t2.start();
}
}
//线程类,代表用户
class AccountThread extends Thread {
private Account act;
public AccountThread(Account act) {
this.act=act;
}
public void run() {
double money=5000;
act.withdraw(money);
System.out.println(act.getActno()+"取款成功,余额"+act.getBalance());
}
}
//账户类,代表银行账户
class Account {
private String actno;
private double balance;//账户余额
public Account() {
}
public Account(String actno, double balance) {
this.setActno(actno);
this.setBalance(balance);
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
// 取款方法
public void withdraw(double money) {
/*加入synchronized关键字后,synchronized用括号包住的代码不能并发
* 单线程t1执行到这以后,若还没结束,另一个线程t2也来到这里,则t2线程要进行等待
* 当t1执行完synchronized下的代码后,t2才可以开始执行这段代码,多个线程原理相同
*/
synchronized(this) {
double before = this.balance;
double after = before - money;
this.setBalance(after);
}
}
}
synchronized的括号中要写是需要同步的线程,即共享的线程对象。上述代码中的共享对象是账户对象,所以是this,而t1和t2都是操作同一个账户对象 act,所以这两个线程要同步。
线程t1执行到synchronized处后,会释放之前占有的时间片,进入锁池找共享对象,此时共享对象this没有被占用,则占有这把锁(一个对象只有一把锁),进入到就绪态;然后线程t2执行到了synchronized,放弃时间片,进入锁池,但共享对象this被占用,t2则排队等待,直到t1执行完synchronized中的代码,释放锁,t2就可以占用这把锁,开始执行synchronized中的代码。
如果在上面加入另一个人对另一个账户取款,即在main方法中加入以下代码:
Account act2=new Account("act001",10000);//初始化银行账户,拥有10000存款
Thread t3=new AccountThread(act2);t3.start();
此时,线程t3无需在t2后面排队,因为t3执行到synchronized处,括号中的共享对象是act2,此时没有线程占用。
synchronized的其他用法
三大变量中,局部变量已经线程安全,实例变量通过synchronized(对象){代码块}或在方法声明中加入synchronized关键字(此时的共享对象是this,代码块是整个方法的内容)可以保证线程安全。
而静态变量加上synchronized,此时的锁是类锁,一个类(假设类名为Myclass)中的静态方法在内存只有一个,存在方法区中,无论创多少个对象,都是只有一个静态方法,故线程不安全,加上synchronized后,多个线程操作多个Myclass对象的synchronized静态方法,虽然是不同的对象,但操作的是静态方法只有一个内存区,类锁只有一个,因此多个线程要等待。
synchronized面试题
问,t2是否要等待t1执行 dosome()完才能执行doother()
public class synch面试题 {
public static void main(String[] args) {
Myclass mc=new Myclass();
Th t1=new Th(mc); Th t2=new Th(mc);
t1.setName("t1"); t2.setName("t2");
t1.start();
try {
Thread.sleep(1*1000);
} catch (InterruptedException e) {
}
t2.start();
}
}
class Th extends Thread{
Myclass mc;
public Th(Myclass mc){
this.mc=mc;
}
public void run() {
if(Thread.currentThread().getName()=="t1") {
mc.dosome();
}else {
mc.doother();
}
}
}
class Myclass{
public synchronized void dosome() {
System.out.println("dosome");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
}
}
public void doother() {
System.out.println("doOther begin");
System.out.println("doOther Over");
}
}
注意:两个线程共享一个对象,若执行不一样的方法,其中一个方法没有synchronized,则该线程不需要等待,因为虽然对象共享,但只有执行到了synchronized,才会去锁池中找锁,否则是异步执行。 线程t2执行的代码没有synchronized,因此不需要排队
因此答案为:不需要等待。
若将Myclass改为如下:
class Myclass{
public synchronized void dosome() {
System.out.println("dosome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("dosome Over");
}
public synchronized void doother() {
System.out.println("doOther begin");
System.out.println("doOther Over");
}
}
答案为:要等待
doOther()也加上synchronized,t2执行到synchronized,要去找锁池找共享对象this,也就是mc,此时mc被t1占用。
若代码再改为:
public class synch面试题 {
public static void main(String[] args) {
Myclass mc=new Myclass();
Myclass mc2=new Myclass();
Th t1=new Th(mc);Th t2=new Th(mc2);
t1.setName("t1"); t2.setName("t2");
t1.start();
try {
Thread.sleep(1*1000);
} catch (InterruptedException e) {
}
t2.start();
}
}
class Th extends Thread{
Myclass mc;
public Th(Myclass mc){
this.mc=mc;
}
public void run() {
if(Thread.currentThread().getName()=="t1") {
mc.dosome();
}else {
mc.doother();
}
}
}
class Myclass{
public static synchronized void dosome() {
System.out.println("dosome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
}
System.out.println("dosome Over");
}
public static synchronized void doother() {
System.out.println("doOther begin");
System.out.println("doOther Over");
}
}
答案为:要等待
此时,两个线程操作不同的对象,但由于静态方法区只有一个,t1执行到 dosome()后,占用了类锁,t2执行到doother(),也需要去找类锁,但此时类锁被t1占用,故要等待。
死锁
public class 死锁 {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
Th1 t1=new Th1(o1,o2);
Th2 t2=new Th2(o1,o2);
t1.start();
t2.start();
}
}
class Th1 extends Thread{
Object o1;
Object o2;
public Th1(Object o1,Object o2) {
this.o1=o1;
this.o2=o2;
}
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(1);
}
}
}
}
class Th2 extends Thread{
Object o1;
Object o2;
public Th2(Object o1,Object o2) {
this.o1=o1;
this.o2=o2;
}
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(2);
}
}
}
}
上述代码中,线程t1和t2共享o1和o2,而t1先占用o1,再占用o2,而t2先占用o2,再占o1.但t1占用o1后还没来的及占o2,o2就被t2占了,于是t1等待t2释放o2,t2等待t1释放o1,这种情况永远不会等到,陷入了死锁状态。