多线程带来的问题
线程安全
当多线程访问一个对象时,若无需考虑下面几点(也就是无需做任何额外考虑,就想单线程那样),调用该对象的行为都可以得到正确的结果,则该对象是线程安全的
- 无需考虑线程在运行时环境下的调度和交替执行
- 无需考虑额外的同步
- 无需考虑调用方进行任何其他的协调操作
要是出现下面这几种情况就可能会出现线程安全问题
- 数据争用:访问共享的变量或资源,由于同时写数据,会造成错误数据
- 数据捆绑:数据之间存在捆绑关系,由于修改未进行同步,会造成错误数据
- 竞争条件:依赖时序的操作,由于顺序原因造成的错误,比如写入前就读取了
非原子操作的数据争用
public static int a;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
//while (a < 10000) a++; //使用while将无法直接知道a++执行的次数
for (int i = 0; i < 10000; i++) a++; //两个线程虽然执行了共执行了a++语句20000次,但a的值最终不一定是20000
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(a);
}
使用下面代码可找出那一步数据写入时出现了数据争用
public static int a;
/*使用原子类,可保证对该数据的操作是原子操作*/
public static AtomicInteger atomicA = new AtomicInteger(0); //与非原子数据进行对比
public static AtomicInteger errorCount = new AtomicInteger(0); //记录出现错误的次数
public static boolean[] mark = new boolean[20001];
/*用来让两个线程在某处同时放行的栅栏*/
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
mark[0] = true;
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
try {
/**
* 保证两个线程同时执行,且都执行完毕a++后再进行标记,
* 以防出现由于该语句不在同步代码块中,而篡改已进入同步代码块中的线程,使用a值的语句,
* CyclicBarrier实例的await方法可以做一个栅栏,若线程不够多时不会向下执行
*/
cyclicBarrier2.reset(); //在等待之前需要重置,以便下次使用重置过了的CyclicBarrier实例
cyclicBarrier1.await(); //等到两个线程执行到此处才会继续向下执行
a++;
cyclicBarrier1.reset(); //在等待之前需要重置
cyclicBarrier2.await(); //等到两个线程执行到此处才会继续向下执行
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
atomicA.incrementAndGet(); //参考值原子性的自增
synchronized (lock) {
if (mark[a] && mark[a - 1]) {
/**
* 由于大部分情况都是未发生冲突的情况,所以大部分情况会让a自增两次,
* 导致a会越过一项,所以只有前一项和当前项都为true时才是出现冲突,
* 由于是从0开始进行对比的,所以第一位应该一开始就设置为true,从而保证第一次就发生冲突时就能判断出来
*/
System.out.println("在[" + a + "]处发生冲突");
errorCount.incrementAndGet(); //错误次数原子性自增
}
mark[a] = true; //每次赋值成功都将对于位置打上true标记
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
/*启动两个子线程*/
thread1.start();
thread2.start();
/*让主线程最后执行结果输出语句*/
thread1.join();
thread2.join();
System.out.println("实际次数(" + a + ") + 冲突次数(" + errorCount + ") = 真正次数(" + atomicA + ")");
}
活跃性问题
- 死锁:两个或多个线程永久阻塞,互相等待对方释放资源
- 饥饿:一个线程因CPU时间全被其他线程抢走而得不到CPU资源
- 活锁:死锁的另一种表现,两个或多个线程并未进入阻塞状态,而是互相避让资源让对方先执行
对象发布与初始化
发布:将对象可超过该类范围之外的地方使用,比如下面这几种情况
- 将一个对象在类是使用
public
修饰 - 方法返回一个对象
- 将对象当作参数传递给一个方法
逸出:对象发布到了不该发布的地方
方法返回私有对象的逸出
返回该私有对象的副本可解决此类线程安全问题
public class ReturnPrivateObject {
private Map<String, String> map;
public ReturnPrivateObject() {
map = new HashMap<>();
map.put("127.0.0.1", "80");
}
public Map<String, String> getMap() {
return map; //直接返回该私有对象
}
public Map<String, String> getMapImproved() {
return new HashMap<>(map); //返回该私有对象的副本
}
public static void main(String[] args) {
ReturnPrivateObject returnPrivateObject = new ReturnPrivateObject();
Map<String, String> map = returnPrivateObject.getMap();
System.out.println("getMapImproved(): " + returnPrivateObject.getMapImproved().get("127.0.0.1"));
returnPrivateObject.getMapImproved().remove("127.0.0.1"); //假设是其他线程做的操作
System.out.println("getMapImproved(): " + returnPrivateObject.getMapImproved().get("127.0.0.1"));
System.out.println("getMap(): " + map.get("127.0.0.1"));
map.remove("127.0.0.1"); //假设是其他线程做的操作
System.out.println("getMap(): " + map.get("127.0.0.1"));
}
}
对象未初始化完就提供外界使用的逸出
构造函数未初始化完就将this赋值
public class Point {
public static Point point;
private int x;
private int y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return x + "," + y;
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
/*由于对象初始化在一个线程中使用在另一个线程中,所以导致未初始化完就使用*/
Thread.sleep(99); //1,0
//Thread.sleep(101); //1,1
System.out.println(Point.point);
}
}
构造函数中新建线程
import java.util.HashMap;
import java.util.Map;
public class InitNewThread {
private Map<String, String> map;
public InitNewThread() {
map = new HashMap<>();
new Thread(() -> {
try {
Thread.sleep(100); //模拟延时初始化
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put("127.0.0.1", "80"); //新建线程中进行初始化
}).start();
}
public Map<String, String> getMap() {
return map; //直接返回该对象
}
public static void main(String[] args) throws InterruptedException {
InitNewThread initNewThread = new InitNewThread();
/*由于对象属性的初始化在另一个线程中,所以导致未初始化完就使用*/
Thread.sleep(99);
//Thread.sleep(101);
System.out.println(initNewThread.getMap().get("127.0.0.1"));
}
}
监听事件中事件触发时机
@FunctionalInterface
interface Event { //事件接口
void onEvent(); //绑定事件方法
}
public class Observer {
static int count; //要初始化的属性
private Event event;
public void registerEvent(Event event) {
this.event = event; //注册事件
}
public void trigger() {
event.onEvent(); //触发事件
}
public static void main(String[] args) throws InterruptedException {
Observer observer = new Observer();
new Thread(() -> {
observer.registerEvent(() -> System.out.println(count)); //该该线程注册事件
try {
Thread.sleep(100); //模拟延时初始化
} catch (InterruptedException e) {
e.printStackTrace();
}
count = 100; //初始化语句
}).start();
/*先让注册线程执行注册,count值未初始化好就触发了事件,count返回0,若初始化好就会返回100*/
Thread.sleep(99);
//Thread.sleep(101);
new Thread(() -> {
observer.trigger(); //该线程中触发事件
}).start();
}
}
使用工厂模式可解决此类线程安全问题
@FunctionalInterface
interface Event { //事件接口
void onEvent(); //绑定事件方法
}
public class Observer {
static int count; //要初始化的属性
private Event event;
private Observer() {
}
public void registerEvent(Event event) {
this.event = event; //注册事件
}
public void trigger() {
event.onEvent(); //触发事件
}
public static Observer getInstance(Event event) { //工厂方法
Observer observer = new Observer();
count = 100; //先进行初始化
observer.registerEvent(event); //再注册事件
return observer;
}
public static void main(String[] args) throws InterruptedException {
Observer observer = Observer.getInstance(() -> System.out.println(count)); //工厂方法将注册和初始化合并
new Thread(() -> {
observer.trigger(); //此时事件触发一定会在注册且初始化之后
}).start();
}
}
多线程性能问题
上下文切换
上下文切换:主要是发生在线程调度时(当可运行线程数超过CPU数量时就会发生线程调度),操作系统内核在CPU上对进程或线程进行以下活动
- 挂起一个进程,将该进程在CPU中的状态存储在内存,进程状态就是上下文
- 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
- 跳转到程序计数器所指向的位置,即跳转到进程被中断时的代码处于,以恢复运行
上下文切换会导致CPU缓存失效,需要重新缓存:程序很大概率会按照一定规律进行数据访问,比如循环语句,CPU会按照一定的算法(LRU算法等)预测下一次要访问的数据进行缓存,从而提高执行效率,一旦发生上下文切换就导致预测失效,就需要重新缓存
频繁抢锁、IO等原因导致频繁阻塞,就会密集型上下文切换
内存同步
为了数据的正确性,同步手段会禁止编译器优化、使CPU内的缓存失效
Comments NOTHING