一、synchronized关键字
多个线程并发读写同一个临界资源时会发生“线程并发安全问题”。常见的临界资源:多线程共享实例变量;多线程共享静态公共变量。
若想解决线程安全问题,需要将异步的操作变为同步操作。异步操作:多线程并发的操作,相当于各干各的;同步操作:有先后顺序的操作,相当于你干完我再干。synchronized关键字是java中的同步锁。
1.1 非静态方法的同步
package day10;
/**
* 多线程并发访问同一资源时,就会形成“抢”的现象。由于线程切换
* 时机不确定,可能导致执行代码顺序的混乱,严重时会导致系统瘫痪。
* @author Administrator
*
*/
public class SyncDemo1 {
public static void main(String[] args) {
final Table table = new Table();
Thread t1 = new Thread() {
public void run() {
while (true) {
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
Thread t2 = new Thread() {
public void run() {
while (true) {
int bean = table.getBean();
Thread.yield();
System.out.println(getName()+":"+bean);
}
}
};
t1.start();
t2.start();
}
}
class Table{
//桌子上有20个豆子
private int beans = 20;
/**
* 当一个方法被synchronized修饰后,该方法为同步方法,即:
* 多个线程不能同时进入方法内部执行。
* 对于成员方法而言,synchronized会在一个线程调用该方法时将该方法
* 所属对象加锁。其他线程在执行该方法时由于执行方法的线程没有释放锁,
* 所以只能在方法外阻塞,直到持有方法锁的线程将方法执行完毕。
* 所以,解决多线程并发执行安全问题的办法就是将“抢”变为“排队”。
* @return
*/
public synchronized int getBean() {
if (beans == 0) {
throw new RuntimeException("没有豆子了!");
}
/*
* static void yield()
* 使当前线程主动让出当次CPU时间片回到Runnable状态,等待分配时间片。
*/
Thread.yield();
return beans--;
}
}
1.2 同步块
package day10;
/**
* 同步块
* 有效的缩小同步范围可以在保证并发安全的同时尽可能提高并发效率。
* @author Administrator
*
*/
public class SyncDemo2 {
public static void main(String[] args) {
final Shop shop = new Shop();
Thread t1 = new Thread() {
public void run() {
shop.buy();
}
};
Thread t2 = new Thread() {
public void run() {
shop.buy();
}
};
t1.start();
t2.start();
}
}
class Shop{
public void buy() {
//获取运行buy方法的线程
Thread t = Thread.currentThread();
try {
System.out.println(t.getName()+":正在挑衣服...");
Thread.sleep(5000);
/**
* 同步块可以要求多个线程对该块内的代码排队执行,但是前提条件是同步
* 监视器对象即(上锁的对象)要求多个线程看到的必须是同一个。
* synchronized(同步监视器对象){
* 需要同步的代码
* }
* 所谓同步执行即:多个线程必须排队执行
* 所谓异步执行即:多个线程可以同时执行
*/
synchronized (this) {
System.out.println(t.getName()+":正在试衣服...");
Thread.sleep(5000);
}
System.out.println(t.getName()+":结账离开...");
} catch (Exception e) {
e.printStackTrace();
}
}
}
每个Java对象都可以用作一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而且无论是通过正常途径退出还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
1.3 静态方法的同步
静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的,原因在于,静态方法锁的是类对象,而非静态方法锁的是当前方法所属对象。
package day10;
/**
* 静态方法的同步
* 当一个静态方法被synchronized修饰后,那么该方法即为同步方法,由于
* 静态方法从属类,全局就一份,所以同步的静态方法一定具有同步效果,与
* 对象无关。
* @author Administrator
*
*/
public class SyncDemo3 {
public static void main(String[] args) {
final Foo f1 = new Foo();
final Foo f2 = new Foo();
Thread t1 = new Thread() {
public void run() {
f1.dosome();//其实该方法为静态方法,与对象无关
}
};
Thread t2 = new Thread() {
public void run() {
f2.dosome();
}
};
t1.start();
t2.start();
}
}
class Foo{
public synchronized static void dosome() {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在运行dosome方法");
Thread.sleep(5000);
System.out.println(t.getName()+":执行dosome方法完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.4 互斥锁
通俗的讲,就是如果一个类里面有多个同步的方法,那么当多线程调用该类中的不同方法时,如果加锁的对象是同一个,那这些线程的执行就是互斥的,只要等一个线程执行完毕后另一个才能执行。
package day10;
/**
* 互斥锁
* synchronized也叫互斥锁,即:
* 使用synchronized修饰多段代码,只要他们的同步监视器
* 对象相同,那么这几段代码间就是互斥关系,即:多个线程不能
* 同时执行这些代码
* @author Administrator
*
*/
public class SyncDemo4 {
public static void main(String[] args) {
final Boo boo = new Boo();
Thread t1 = new Thread() {
public void run() {
boo.methodA();
}
};
Thread t2 = new Thread() {
public void run() {
boo.methodB();
}
};
t1.start();
t2.start();
}
}
class Boo{
public synchronized void methodA() {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在执行A方法");
Thread.sleep(5000);
System.out.println(t.getName()+":A方法执行完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void methodB() {
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在执行B方法");
Thread.sleep(5000);
System.out.println(t.getName()+":B方法执行完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
}
二、线程安全API与非线程安全API
StringBuffer是同步的,synchronized append();StringBuilder不是同步的append()。
Vector和Hashtable是同步的,ArrayList和HashMap不是同步的。
获取线程安全的集合方式:
Collections.synchronizedList(),
Collections.synchronizedSet()。
获取线程安全的Map:
Collections.synchronizedMap()。
package day10;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 将集合或Map转换为线程安全的
* @author Administrator
*
*/
public class SyncDemo5 {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("one");
list.add("two");
list.add("three");
list.add("four");
System.out.println(list);
//将给定集合转换为线程安全的集合
list = Collections.synchronizedList(list);
System.out.println(list);
/*
* HashSet不是线程安全的
*/
Set<String> set = new HashSet<String>(list);
System.out.println(set);
//将给定的set集合转换为线程安全的
set = Collections.synchronizedSet(set);
System.out.println(set);
/*
* HashMap也不是线程安全的
*/
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("语文", 99);
map.put("数学", 98);
map.put("英语", 97);
System.out.println(map);
map = Collections.synchronizedMap(map);
System.out.println(map);
/*
* API手册上有说明就算是线程安全的集合,但其中对于元素的操作,
* 如:add,remove等方法都不与迭代器遍历做互斥,需要自行
* 维护互斥关系。
*/
}
}
三、使用ExecutorService实现线程池
ExecutorService是java提供的用于管理线程池的类。
线程池的概念:首先创建一些线程,他们的集合称为线程池,当服务器接收到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。
在线程池的编程模式下,任务是提交给整个线程池的,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程。一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
线程池有以下几种实现策略:
1、Executors.newCachedThreadPool(),创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
2、Executors.newFixedThreadPool(int nThreads),创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
3、Executors.newScheduledThreadPool(int corePoolSize),创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行。
4、Executors.newSingleThreadExecutor(),创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。
package day10;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
* 线程池主要有两个作用:
* 1、重用线程
* 2、控制线程数量
* 当我们的应用需要创建大量线程或者发现线程会频繁的创建和销毁时就
* 应当考虑使用线程池来维护线程。
* @author Administrator
*
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0;i < 5;i++) {
Runnable runn = new Runnable() {
@Override
public void run() {
Thread t = Thread.currentThread();
try {
System.out.println(t+":正在运行任务");
Thread.sleep(5000);
System.out.println(t+":运行任务完毕");
} catch (Exception e) {
System.out.println("线程被中断了");
}
}
};
threadPool.execute(runn);
System.out.println("指派了一个任务交给线程池");
}
threadPool.shutdown();//执行完任务停止
//threadPool.shutdownNow();//立刻马上停止
System.out.println("停止线程池了");
}
}
四、使用BlockingQueue
BlockingQueue是双缓冲队列。
在多线程并发时,若需要使用队列,我们可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。
BlockingQueue内部使用两条队列,可允许两个线程同时向队列进行操作,一个做存储,一个做取出操作,在保证并发安全的同时了队列的存取效率。