java线程同步处理并发问题
一个对象同时被多个线程读写时,会造成并发问题,线程同步把这些线程排队来解决这个问题。
线程同步有两个元素,队列和锁。
锁
人为对被修改的对象加锁,使对象每次只能被一个线程修改,一个线程获得排他锁的时候获得读写机会,其他线程被挂起,这个线程读写完后释放锁。
只有进行修改操作时才需要锁,其他操作不需要锁,否则低效。
死锁
死锁是锁机制错误使用的情况。多个线程在已经持有一个资源的锁时,还想获得另一个资源的锁,就可能发生死锁。
以下代码展示死锁发生的情况:
package com.cxf.multithread.lock;
public class TestForLock {
public static void main(String[] args) {
Girl girl1 = new Girl(0,"snow white");
Girl girl2 = new Girl(1,"snow pink");
girl1.start();
girl2.start();
}
}
class Lipstick{}
class Mirror{}
class Girl extends Thread {
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String name;
Girl(int choice, String name) {
this.choice = choice;
this.name = name;
}
public void run(){
try {
get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void get() throws InterruptedException {
if(choice == 0) {
synchronized (lipstick) {
System.out.println(name + " get lipstick");
Thread.sleep(1000);
synchronized (mirror) {
System.out.println(name + " get mirror");
}
}
}else {
synchronized (mirror) {
System.out.println(name + " get mirror");
Thread.sleep(1000);
synchronized (lipstick) {
System.out.println(name + " get lipstick");
}
}
}
}
}
输出结果:
snow white get lipstick
snow pink get mirror
线程1持有实例1的锁,同时想获得实例2的锁,得不到便无法释放实例1的锁。
线程2持有实例2的锁,同时想获得实例1的锁,得不到便无法释放实例2的锁。
由此产生死锁。
解决办法:
package com.cxf.multithread.lock;
public class TestForLock {
public static void main(String[] args) {
Girl girl1 = new Girl(0,"snow white");
Girl girl2 = new Girl(1,"snow pink");
girl1.start();
girl2.start();
}
}
class Lipstick{}
class Mirror{}
class Girl extends Thread {
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String name;
Girl(int choice, String name) {
this.choice = choice;
this.name = name;
}
public void run(){
try {
get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void get() throws InterruptedException {
if(choice == 0) {
synchronized (lipstick) {
System.out.println(name + " get lipstick");
Thread.sleep(1000);
}
synchronized (mirror) {
System.out.println(name + " get mirror");
}
}else {
synchronized (mirror) {
System.out.println(name + " get mirror");
Thread.sleep(1000);
synchronized (lipstick) {
System.out.println(name + " get lipstick");
}
}
}
}
}
输出结果:
snow white get lipstick
snow pink get mirror
snow pink get lipstick
snow white get mirror
只要有一个线程不同时想要持有两个锁,死锁就不会产生。
可重入锁
可重入锁是显式锁,开启锁后需要关闭。而synchronized是隐式锁,自动关闭。
不加锁买票
以下代码展示不加锁买票的情况。
package com.cxf.multithread.relock;
public class TestForRelock {
public static void main(String[] args) {
GetTicket getTicket = new GetTicket();
new Thread(getTicket).start();
new Thread(getTicket).start();
new Thread(getTicket).start();
}
}
class GetTicket implements Runnable {
int num = 10;
@Override
public void run() {
while (true) {
if (num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get NO."+ num-- +" ticket");
}else break;
}
}
}
输出结果:
get NO.10 ticket
get NO.9 ticket
get NO.8 ticket
get NO.7 ticket
get NO.7 ticket
get NO.7 ticket
get NO.6 ticket
get NO.6 ticket
get NO.5 ticket
get NO.4 ticket
get NO.4 ticket
get NO.4 ticket
get NO.3 ticket
get NO.3 ticket
get NO.3 ticket
get NO.2 ticket
get NO.2 ticket
get NO.2 ticket
get NO.1 ticket
get NO.1 ticket
get NO.1 ticket
买到了重复的票。
加锁买票
package com.cxf.multithread.relock;
import java.util.concurrent.locks.ReentrantLock;
public class TestForRelock {
public static void main(String[] args) {
GetTicket getTicket = new GetTicket();
new Thread(getTicket).start();
new Thread(getTicket).start();
new Thread(getTicket).start();
}
}
class GetTicket implements Runnable {
int num = 10;
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("get NO."+ num-- +" ticket");
}else break;
}finally {
lock.unlock();
}
}
}
}
输出结果:
get NO.10 ticket
get NO.9 ticket
get NO.8 ticket
get NO.7 ticket
get NO.6 ticket
get NO.5 ticket
get NO.4 ticket
get NO.3 ticket
get NO.2 ticket
get NO.1 ticket
不会买到重复的票。
可重入锁使用ReenTrantLock类建立锁实例,方法lock加锁,unlock解锁,一般try finally格式来执行:
try{
lock();
访问对象的语句;
}finally{unlock}
不安全的例子
下面3个不安全的例子反映出并发问题。
买票
以下代码展示多个人买同一种票的情况:
package com.cxf.multithread.problem.ticket;
public class TestForTicket {
public static void main(String[] args) {
ByTicket getByAll = new ByTicket();
Thread thread1 = new Thread(getByAll,"cxf");
Thread thread2 = new Thread(getByAll,"cdf");
Thread thread3 = new Thread(getByAll,"czf");
thread1.start();
thread2.start();
thread3.start();
}
}
class ByTicket implements Runnable{
private int num = 10;
boolean flag =true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
if (num <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" get NO."+num--+" ticket");
}
}
输出结果:
cxf get NO.10 ticket
cdf get NO.8 ticket
czf get NO.9 ticket
czf get NO.7 ticket
cxf get NO.7 ticket
cdf get NO.6 ticket
czf get NO.4 ticket
cxf get NO.3 ticket
cdf get NO.5 ticket
cxf get NO.2 ticket
czf get NO.0 ticket
cdf get NO.1 ticket
有人买到了相同的票,也有人买到了不存在的票。
取钱
以下代码演示2个人同时在一个银行账户上取钱的情况:
package com.cxf.multithread.problem.bank;
public class TestForBank {
public static void main(String[] args) {
Account account = new Account(100,"All");
GetMoney cxf = new GetMoney(account,60,"cxf");
GetMoney cdf = new GetMoney(account,70,"cdf");
cxf.start();
cdf.start();
}
}
class Account{
int remain;
String name;
public Account(int remain,String name) {
this.remain = remain;
this.name =name;
}
}
class GetMoney extends Thread{
Account account;
int toGet;
public GetMoney(Account account,int toGet,String name) {
super(name);
this.account = account;
this.toGet = toGet;
}
public void run() {
if (account.remain - toGet < 0) {
System.out.println(Thread.currentThread().getName()+" not enough remain");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.remain = account.remain - toGet;
System.out.println(account.remain + " yuan left");
System.out.println(this.getName() + " got " + toGet);
}
}
输出结果:
-30 yuan left
-30 yuan left
cxf got 60
cdf got 70
发生了超额取款的情况。
记录
以下代码演示用列表记录多个线程时会发生的事情:
package com.cxf.multithread.problem.list;
import java.util.ArrayList;
import java.util.List;
public class TestForList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(500);
System.out.println(list.size());
}
}
输出结果:
99993
正确结果应为100000。实质上也是多个线程操作同一个对象。
使用同步块对例子改进
正确买票
以下代码演示如何正确买票:
package com.cxf.multithread.problem.ticket;
public class TestForTicket {
public static void main(String[] args) {
ByTicket getByAll = new ByTicket();
Thread thread1 = new Thread(getByAll,"cxf");
Thread thread2 = new Thread(getByAll,"cdf");
Thread thread3 = new Thread(getByAll,"czf");
thread1.start();
thread2.start();
thread3.start();
}
}
class ByTicket implements Runnable{
private int num = 10;
boolean flag =true;
@Override
public void run() {
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized private void buy() throws InterruptedException {
if (num <= 0) {
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" get NO."+num--+" ticket");
}
}
输出结果:
cxf get NO.10 ticket
cxf get NO.9 ticket
cxf get NO.8 ticket
cxf get NO.7 ticket
cxf get NO.6 ticket
cxf get NO.5 ticket
cxf get NO.4 ticket
cxf get NO.3 ticket
cxf get NO.2 ticket
cxf get NO.1 ticket
同步块的关键词synchronized加在buy方法的定义前面。相当于对含有buy方法的实例this加锁。
正确取钱
以下代码演示如何正确取钱:
package com.cxf.multithread.problem.bank;
public class TestForBank {
public static void main(String[] args) {
Account account = new Account(100,"All");
GetMoney cxf = new GetMoney(account,60,"cxf");
GetMoney cdf = new GetMoney(account,70,"cdf");
cxf.start();
cdf.start();
}
}
class Account{
int remain;
String name;
public Account(int remain,String name) {
this.remain = remain;
this.name =name;
}
}
class GetMoney extends Thread{
Account account;
int toGet;
public GetMoney(Account account,int toGet,String name) {
super(name);
this.account = account;
this.toGet = toGet;
}
public void run() {
synchronized (account) {
if (account.remain - toGet < 0) {
System.out.println(Thread.currentThread().getName()+" not enough remain");
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.remain = account.remain - toGet;
System.out.println(account.remain + " yuan left");
System.out.println(this.getName() + " got " + toGet);
}
}
}
输出结果:
40 yuan left
cxf got 60
cdf not enough remain
同步块关键字synchronized放在方法内部,synchronized后()内为被加锁的对象,{}内为被锁限制的访问修改动作。
正确记录
以下代码演示如何正确记录:
package com.cxf.multithread.problem.list;
import java.util.ArrayList;
import java.util.List;
public class TestForList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(500);
System.out.println(list.size());
}
}
输出结果:
100000
这个例子是以列表为操作对象,因此还有另一种办法实现线程同步:
package com.cxf.multithread.problem.list;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class TestForList {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(500);
System.out.println(list.size());
}
}
输出结果:
100000
这里不用List生成列表,而是用CopyOnWriteArrayList,自带同步功能。