1.为什么线程安全是重点?
项目是在服务器中运行,而线程的定义、线程对象的创建、线程的启动并不需要我们编写代码。我们需要关注的是,自己编写的程序在多线程并发的环境下是否安全。
2.什么时候数据在多线程并发的情况下会出现安全问题?
条件1:多线程并发
条件2:有共享数据
条件3:共享数据有修改行为
同时具备以上三个条件就会出现线程安全问题。
3.怎么解决线程安全问题?
用线程排队执行解决线程安全问题,这种机制称为:“线程同步机制”,简称线程同步,提高安全性降低效率。
4.线程异步和线程同步
异步编程模型:线程t1和线程t2各自执行各自的,谁也不需要等谁,实际上就是多线程并发。
同步编程模型:线程t1和线程t2需要互相等待,同步就是排队,效率较低。
5.模拟银行取钱(不使用线程同步)
账户类:
public class Acount {
private String user;//账户
private double banlence;//余额
public Acount() {
}
public Acount(String user, double banlence) {
this.user = user;
this.banlence = banlence;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public double getBanlence() {
return banlence;
}
public void setBanlence(double banlence) {
this.banlence = banlence;
}
//取款方法
public void withdrew(double money){
//取款前的余额
double before=this.getBanlence();
//取款后的余额
double after=before-money;
//更新余额 当前线程还未设置余额时另外一个线程进入本方法就会出问题
setBanlence(after);
}
}
取款行为:
public class AcountThread extends Thread{
//两个线程共享同一个账户
private Acount act=null;
public AcountThread(Acount act){
this.act=act;
}
@Override
public void run() {
//取款5000
act.withdrew(5000);
System.out.println("账户"+act.getUser()+"取款成功!取款金额"+act.getBanlence());
}
}
测试两个线程同时取款:
public class Test {
public static void main(String[] args) {
Acount acount=new Acount("张三",100000);
Thread thread1=new AcountThread(acount);
Thread thread2=new AcountThread(acount);
thread1.start();
thread2.start();
}
}
6.使用线程同步机制synchronized 取款
//取款方法
public void withdrew(double money){
//以下代码应该排队,不能并发
//取款前的余额
/**
* synchronized (这个括号中的参数十分重要) {
* }
*
* 参数该怎么写?假设t1,t2,t3,t4,t5共用5个线程,只希望前三个排队,后两个并发。
* 则参数应该写成t1,t2,t3共享的对象并且对于t4,t5来说不是共享的
*
* synchronized的括号里面写共享对象!共享对象!共享对象!
*/
synchronized (this) {
double before = this.getBanlence();
//取款后的余额
double after = before - money;
//更新余额 当前线程还未设置余额时另外一个线程进入本方法就会出问题
setBanlence(after);
}
}
synchronized 原理:当线程遇到synchronized 关键字,就会去找共享对象的对象锁,找到之后占有这把锁,其他线程无法进入,直到执行完同步代码块释放锁,下一个等待的线程就可以进入。
稍作修改:当obj作为一个实例变量的时候,也是被共享的,并且每个账户只有一个obj(若obj为局部变量就不可以,因为每次调方法就会new一个新对象),所以可以作为synchronized 的参数。哪怕写个字符串“abc”都行,因为这是在字符串常量池中共享且唯一的。
public class Acount {
private String user;//账户
private double banlence;//余额
Object obj=new Object();
.....
//取款方法
public void withdrew(double money){
synchronized (obj) {
double before = this.getBanlence();
//取款后的余额
double after = before - money;
//更新余额 当前线程还未设置余额时另外一个线程进入本方法就会出问题
setBanlence(after);
}
}
}
7.哪些变量有线程安全问题【重要】
1.静态变量---->存储在方法区【共享,不安全】
2.实例变量---->存储在堆内存【共享,不安全】
3.局部变量---->存储在栈内存【不共享,安全,一个线程一个栈】
所以,局部变量和常量不会有线程安全问题,成员变量可能会有线程安全问题。
8.synchronized 放在实例方法上
当synchronized 放在实例方法上,锁的一定是this,不可能是其他对象,所以这种方式不灵活;而且整个方法体都会同步,可能会无故扩大同步的范围,导致程序的执行效率较低。
如果共享的对象是this,而且要同步的代码块是整个方法体,那么建议使用这种方式。
public synchronized void withdrew(double money){
double before = this.getBanlence();
//取款后的余额
double after = before - money;
//更新余额 当前线程还未设置余额时另外一个线程进入本方法就会出问题
setBanlence(after);
}
例如StringBuffer线程安全,StringBuilder线程不安全,所以在使用局部变量时,由于局部变量也是线程安全的,所以可以使用StringBuilder。
ArrayList是非线程安全的
Vector是线程安全的
HashMap、Hashset是非线程安全的
HashTable是线程安全的
9.小练习
1.一个类中,一个方法有synchronized,一个方法没有synchronized,两个线程分别执行两个方法时是否会排队。
上锁对象:
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---->end");
}
public void doOther(){
System.out.println("doOther---->begin");
System.out.println("doOther---->end");
}
}
调用方法:
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc=mc;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
public class Test01 {
public static void main(String[] args) {
MyClass myClass=new MyClass();
MyThread myThread1 = new MyThread(myClass);
MyThread myThread2 = new MyThread(myClass);
myThread1.setName("t1");
myThread2.setName("t2");
myThread1.start();
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread2.start();
}
}
答案:不会排队,doOther()方法没有synchronized 关键字,不会去找对象锁,直接执行。
2.将上述代码的doOther()方法加上synchronized 关键字,是否会排队
答案:会排队,两个方法锁住的是同一个对象mc,所以要排队。
3.doOther()方法加上synchronized 关键字并且实例化两个MyClass对象分别给两个线程,是否还会排队。
答案:不会排队,锁住的不是同一个对象。
4.将synchronized方法加上static关键字变为静态方法,依旧实例化两个MyClass对象给两个线程,会不会排队?
答案:会排队,静态方法是类锁,不管对象有多少个,类锁只有一把,需要等待。
class MyClass{
public synchronized static void doSome(){
System.out.println("doSome---->begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome---->end");
}
public synchronized static void doOther(){
System.out.println("doOther---->begin");
System.out.println("doOther---->end");
}
}
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc=mc;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
public class Test01 {
public static void main(String[] args) {
MyClass myClass1=new MyClass();
MyClass myClass2=new MyClass();
MyThread myThread1 = new MyThread(myClass1);
MyThread myThread2 = new MyThread(myClass2);
myThread1.setName("t1");
myThread2.setName("t2");
myThread1.start();
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread2.start();
}
}
10.什么是死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
11.产生死锁的条件
1.互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2.请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4.环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
避免死锁:不要嵌套使用synchronized 。
public class DeadLock {
public static void main(String[] args) {
Object o1=new Object();
Object o2=new Object();
//两个线程共享o1,o2
Thread t1= new myThread01(o1,o2);
Thread t2= new myThread02(o1,o2);
t1.start();
t2.start();
}
}
class myThread01 extends Thread{
Object o1;
Object o2;
public myThread01(Object o1, Object o2) {
this.o2 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class myThread02 extends Thread{
Object o1;
Object o2;
public myThread02(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
12.在开发中怎么解决线程安全问题
大量使用synchronized 会让程序效率降低,不到万不得已别用。
方案一:尽量使用局部变量代替实例变量和静态变量。
方案二:如果必须使用实例变量,那么尽量创建多个对象,这样对象不共享,一个线程一个对象。
方案三:如果不能使用局部变量也不能创建多个对象,那就使用synchronized (可以写在方法内,实例方法上,静态方法上)
13.守护线程
java中有两种线程:用户线程和守护线程,例如JVM的垃圾回收就是守护线程。用户线程一结束守护线程就会结束,哪怕守护线程是个死循环。
通过setDaemon(true)方法设置守护线程,要在线程开启之前。
t1.setDaemon(true);
t1.start();