脏读
我们经常会遇到要同时在数据库中修改几个数据的场景。下面列举了一种场景,一个线程正在修改账号密码,另一个线程在读取账号密码,这个时候,会出现并发引起的脏读的问题,有可能,第一个线程刚把账号改完,密码还没改过了,而另一个线程就在读取账号和密码了,这个时候数据就是不统一的。我们来看下面这段有问题的代码。
public class Person{
private String username = "1";
private String password = "1";
synchronized public void setValue(String username,String password) throws InterruptedException{
this.username = username;
Thread.sleep(5000);
this.password = password;
}
public String getValue(){
return "username=" + this.username + " password=" + this.password;
}
}
//测试
public class Test{
public static void main(String[] args) {
final Person p = new Person();
Thread t1 = new Thread(){
@Override
public void run() {
try {
p.setValue("a", "a");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
};
Thread t2 = new Thread(){
@Override
public void run() {
System.out.println(p.getValue());
};
};
t1.start();
t2.start();
}
}
我们看到,如果是多个线程修改密码,这是线程安全的,因为,它前面加了synchronized关键字,但是由于getValue()是不同步的,这个在setValue()还没执行完,就获取数据,这个时候,线程就不安全了,我们做如下更改。
public class Person{
private String username = "1";
private String password = "1";
synchronized public void setValue(String username,String password) throws InterruptedException{
this.username = username;
Thread.sleep(5000);
this.password = password;
}
synchronized public String getValue(){
return "username=" + this.username + " password=" + this.password;
}
}
做如上修改之后,a线程先进行setValue(),由于这个方法前面加了synchronized关键字,所以,当t1线程执行的时候,它哪到了对象锁,如果其他同步方法想调用这个对象,要等t1线程先释然锁,然后再执行,笔者观察到,输出结果在执行后5秒才打印的,也就证明了这一点。
锁重入
两个加了synchronized的方法能不能相互调用呢?我们来看一个例子。
class User{
private String username;
private String password;
private String mail;
synchronized public void setUser(String username,String password){
this.username = username;
this.password = password;
}
synchronized public void setUser(String username,String password,String mail){
this.setUser(username, password); //这里直接调用synchronized关键字修饰的方法
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.mail = mail;
}
synchronized public String getUser(){
return "username="+ this.username + "\npassword" + this.password + "\nmail=" + this.mail;
}
}
public class Test{
public static void main(String[] args) throws InterruptedException {
final User p = new User();
Thread t1 = new Thread(){
@Override
public void run() {
p.setUser("a", "a","123@163.com");
};
};
Thread t2 = new Thread(){
@Override
public void run() {
p.setUser("b", "b","234@163.com");
};
};
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(p.getUser());
}
}
输出结果:
username=a
password=a
mail=123@163.com
输出结果并没有发生冲突,也就是说,是username和password是对应的。同时也证明了能在synchronized中修饰的方法中调用其他synchronized修饰的方法。
遇到异常,锁会释放
public class Test{
public static void main(String[] args) throws InterruptedException {
final User2 p = new User2();
Thread t1 = new Thread(){
@Override
public void run() {
try {
p.setUser("a", "a");
} catch (Exception e) {
e.printStackTrace();
}
};
};
Thread t2 = new Thread(){
@Override
public void run() {
try {
p.setUser("b", "b");
} catch (Exception e) {
e.printStackTrace();
}
};
};
t1.start();
t2.start();
Thread.sleep(500);
System.out.println(p.getUser());
}
}
class User{
private String username;
private String password;
synchronized public void setUser(String username,String password) throws Exception{
this.username = username;
if(username.equals("a")) throw new Exception();
Thread.sleep(5000);
this.password = password;
}
synchronized public String getUser(){
return "username=" + this.username + "\npassword=" + this.password;
}
}
class User2 extends User{
}
输出结果:
java.lang.Exception
at User.setUser(Test.java:39)
at Test$1.run(Test.java:8)
username=b
password=b
这个输出结果一方面证明了,synchronized修饰的方法,会继承到子类,但是如果子类中重写了该方法,并且没有使用synchronized修饰,那么这个方法就是不同步的。以上结果只等待了5秒,也证明了,如果遇到异常,锁会释放。
一半异步一半同步来提高执行效率
可以通过一半异步和一半同步的方法来提高执行效率,仅仅把小部分需要同步的地方进行同步。
我们先来以前直接在函数上添加synchronized关键字的方法来同步执行。
public class Test{
public static void main(String[] args) {
final Service service = new Service();
Thread t1 = new Thread("A"){
@Override
public void run() {
try {
service.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread("B"){
@Override
public void run() {
try {
service.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
}
}
class Service{
String start;
String end;
synchronized public void a() throws InterruptedException{
long str1 = System.currentTimeMillis();
Thread.sleep(3000); //这里可能还有很长一段的其他操作,这里抽象成一个线程睡眠
long str2 = System.currentTimeMillis();
String name = Thread.currentThread().getName();
start=name + " - start - " + str1;
end= name + "- end - "+str2;
System.out.println(start);
System.out.println(end);
}
}
我们可以发现输出结果为:
A - start - 1527776484547
A- end - 1527776487547
B - start - 1527776487547
B- end - 1527776490547
也就是说,一定要等A执行完了才能执行B函数。事实上,有可能在str1和str2之间的那段代码,有可能是不要求同步的,那么这也就让B白白等待了3秒,降低了执行效率。
下面来看一半异步一半同步的解决方案。
public class Test{
public static void main(String[] args) {
final Service service = new Service();
Thread t1 = new Thread("A"){
@Override
public void run() {
try {
service.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread("B"){
@Override
public void run() {
try {
service.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
}
}
class Service{
String threadName;
String start;
String end;
public void a() throws InterruptedException{
long str1 = System.currentTimeMillis();
Thread.sleep(3000); //这里可能还有很长一段的其他操作,这里抽象成一个线程睡眠
long str2 = System.currentTimeMillis();
synchronized (this) {
threadName = Thread.currentThread().getName();
start=threadName + " - start - " + str1;
end= threadName + "- end - "+str2;
System.out.println(start);
System.out.println(end);
}
}
}
输出结果:
B - start - 1527777631172
B- end - 1527777634173
A - start - 1527777631172
A- end - 1527777634173我们可以发现,两个是线程几乎是同时完成的。这在一定程度上提高了执行效率。
证明synchronized是锁定的当前对象
如果有多个方法,都加了锁synchronized(this)锁。那么调用顺序是怎样的呢?我们来看如下例子。
public class Test{
public static void main(String[] args) {
final Service service = new Service();
Thread t1 = new Thread("A"){
@Override
public void run() {
try {
service.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread("B"){
@Override
public void run() {
service.b();
}
};
t1.start();
t2.start();
}
}
class Service{
public void a() throws InterruptedException {
synchronized (this) {
System.out.println(System.currentTimeMillis());
Thread.sleep(5000);
}
}
public void b(){
synchronized (this) {
System.out.println(System.currentTimeMillis());
}
}
}
输出结果:
1527779448736
1527779453736
发现,两个线程并不是异步执行的,而是等一个线程执行完之后再执行的另外一个线程,这就证明了synchronized(this)是锁定的当前对象。
synchronized(非this对象)
使用synchronized(非this对象)同样也能达到同步访问效果,如下这种脏读现象就能用这种方式解决。
import java.util.ArrayList;
import java.util.List;
public class Test{
public static void main(String[] args) {
final MyList<String> list = new MyList<String>();
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
list.a("1");
}
super.run();
}
};
Thread t2 = new Thread(){
public void run() {
System.out.println(list.getSize());
};
};
t1.start();
t2.start();
}
}
class MyList<T>{
List<T> list = new ArrayList<T>();
public void a(T username){
synchronized (this) {
list.add(username);
}
}
public int getSize(){
synchronized (this) {
return list.size();
}
}
}
输出结果:361
如果希望等待所有add方法执行完成之后,再执行getSize方法,那么使用synchronized(非this)锁
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
final MyList<String> myList = new MyList<String>();
Thread t1 = new Thread(){
@Override
public void run() {
synchronized (myList) {
for (int i = 0; i < 1000; i++) {
myList.add("1");
}
}
}
};
Thread t2 = new Thread(){
public void run() {
System.out.println(myList.getSize());
};
};
t1.start();
t2.start();
}
}
class MyList<T>{
List<T> list = new ArrayList<T>();
synchronized public void add(T t){
list.add(t);
}
synchronized public int getSize(){
return list.size();
}
}
对象锁的结论
当一个对象中含有synchronized(this)、synchronized 修饰成员函数、synchronized(非this锁)三种锁的时候,同时被几个
synchronized锁定class
这种锁锁定的是整个class,它可以让各个静态函数之间同步,但是它与非静态函数之间任然是异步,也就是说,synchronized在锁定静态资源的情况下,它只能让访问的线程静态资源产生同步的效果。
public class Test {
public static void main(String[] args) {
final Service s = new Service();
Thread t1 = new Thread(){
@Override
public void run() {
try {
Service.a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
try {
s.c();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t3 = new Thread(){
@Override
public void run() {
try {
Service.b();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
t3.start();
}
}
class Service{
synchronized public static void a() throws InterruptedException{
System.out.println("aaaaa start" + System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("aaaaa end" + System.currentTimeMillis());
}
synchronized public static void b() throws InterruptedException{
synchronized(Service.class){
System.out.println("bbbbb start" + System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("bbbbb end" + System.currentTimeMillis());
}
}
synchronized public void c() throws InterruptedException{
System.out.println("ccccc start" + System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("ccccc end" + System.currentTimeMillis());
}
}
我们可以看出输出结果为:
ccccc start1528079860918
bbbbb start1528079860918
bbbbb end1528079863919
ccccc end1528079863919
aaaaa start1528079863919
aaaaa end1528079866919
根据如上输出结果,很明显,当一个函数是静态函数并且被synchronized修饰的时候,该类产生的所有对象,对静态函数的访问都具有同步效果。很明显c()函数没有同步的效果,原因是该函数不是静态资源。
死锁–无限等待
当一个线程一直占用着当前对象的锁,那么另一个线程只能一直等着,永远得不到执行,这也是一种死锁的情况。
那么如何解决这种死锁的情况呢?我们使用两把锁就可以解决这个问题。如下例。
public class Test{
public static void main(String[] args) {
final Service service = new Service();
Thread t1 = new Thread(){
@Override
public void run() {
service.methodA();
}
};
Thread t2 = new Thread(){
@Override
public void run() {
service.methodB();
}
};
t1.start();
t2.start();
}
}
class Service{
Object o1 = new Object();
public void methodA(){
synchronized (o1) { //锁1
System.out.println("thread - methodA - start");
boolean flag = true;
while(flag){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("thread - methodA - end");
}
}
Object o2 = new Object();
public void methodB(){ //锁2
synchronized (o2) {
System.out.println("thread - methodB - start");
System.out.println("thread - methodB - start");
}
}
}
输出结果:
thread - methodB - start
thread - methodB - start
thread - methodA - start
死锁–双锁互占
双锁互占的情况就是,线程1占用着锁1,线程2占用着锁2,但是线程1中的锁1中又用到了锁2,它需要拿到锁2执行完成,自己才能被释放,而线程2中的锁2又用到了锁1,它需要拿到锁1执行完成之后,自己才能被释放,这样的情况就是双锁互相等待释放情况。
public class Test{
public static void main(String[] args) {
final Service service = new Service();
Thread t1 = new Thread(){
@Override
public void run() {
try {
service.methodA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
try {
service.methodB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
}
}
class Service{
Object lock1 = new Object();
Object lock2 = new Object();
public void methodA() throws InterruptedException{
synchronized (lock1) { //锁1
System.out.println("thread - methodA - start");
Thread.sleep(3000);
synchronized (lock2) {
System.out.println("methodA - lock2 start - end");
}
System.out.println("thread - methodA - end");
}
}
public void methodB() throws InterruptedException{ //锁2
synchronized (lock2) {
System.out.println("thread - methodB - start");
Thread.sleep(3000);
synchronized (lock1) {
System.out.println("methodB - lock1 start-end");
}
System.out.println("thread - methodB - start");
}
}
}
输出结果:
thread - methodB - start
thread - methodA - start
在jdk中提供了一种查看有没有死锁的办法,运行程序后,我们切换都jdk的bin目录,运行cmd,然后输入jps命令,得到Run的线程,然后再输入jstack -l run线程的id,然后就能在控制台看到死锁的详情了。
锁变向
当一个线程拿到锁之后,可以换锁,让该锁给其他线程使用。如下例。
public class Test{
public static void main(String[] args) throws InterruptedException {
final Service service = new Service();
Thread t1 = new Thread(){
@Override
public void run() {
try {
service.methodA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
service.methodB();
}
};
t1.start();
Thread.sleep(50);
t2.start();
}
}
class Service{
String lock1 = "aaa";
public void methodA() throws InterruptedException{
synchronized (lock1) {
System.out.println("MethodA start");
lock1 = "bbb"; //改变锁定向
Thread.sleep(5000);
System.out.println("MethodA end");
}
}
public void methodB(){
synchronized (lock1) {
System.out.println("MethodB start");
System.out.println("MethodB end");
}
}
}
输出结果:
MethodA start
MethodB start
MethodB end
MethodA end
Thread.sleep(50);是必要的,如果没有这句,那么t1.start();和t2.start();,那么t1和t2都在争抢锁,当抢不到该锁,那么它就等待t1执行完成才能执行t2或者相反。
这里是用常量来当锁,如果是其他对象,改变对象的属性,而未改变对象的指向,达不到上面的效果。