差不多做了一段时间的java开发了,然后其实项目上高并发用到的并不多。就只有自己来学习一下了,网上搜了下资料,然后自己总结了一下。下面开始正题。
现目前许多项目都会涉及到高并发的问题,先来说下线程和进程。
进程:操作系统中正在运行的程序。
线程:是进程中的一个执行流程;
用一句话来说,解决高并发问题,就是解决 多线程对某个资源的有序访问和修改。避免脏读,幻读等。通常来说数据库也可以解决这类的问题,表锁,行锁,对其数据进行读锁或者写锁操作。这些都是题外话了,后面会针对数据库的操作写一篇博客。
言归正传。
现在涉及两个简单的场景:就用卖票为例吧
1、两个窗口,每个窗口有100张车票。互不干扰
2、两个窗口一起卖100张票,卖完为止。
在此之前,使用线程的方法基本上常用的就是继承Thread,实现runnable。
先说第一种情况吧,我们先继承Thread
public class TicketsThread extends Thread{
//设置总票数
private int num = 100;
@Override
public void run() {
//多线程中建议使用while
while (true)
{
if(num>0)
{
num--;
//Thread.currentThread().getName()得到当前线程的名称
System.out.println(Thread.currentThread().getName()+"卖了张票,还剩"+num);
}
}
}
}
然后写一个test去操作它
public static void main(String args[])
{
//分别卖一百张票
//窗口1
TicketsThread threadTest1 = new TicketsThread();
//窗口2
TicketsThread threadTest2 = new TicketsThread ();
threadTest1.setName("窗口1");
threadTest2.setName("窗口2");
threadTest1.start();
threadTest2.start();
}
ok,然后执行
然后说第二种情况,同用100张票。
之前我们说过了 解决并发问题就是为了 解决多线程对某个资源的有序访问和修改!!!
我们接下来就是为了解决这个问题 : 解决多线程对某个资源的有序访问和修改!!!(记住这句话)
有的同学可能猜到了,我们会使用同步锁synchronized,那么恭喜你,猜错了。这里我想先说下volatile。
我相信,有的同学会使用volatile来修饰这个变量,但是有些时候volatile并不能解决我们刚刚说的问题(解决多线程对某个资源的有序访问和修改),重要的事情说三遍,记住这句话,句话,话!
我们先把代码展示出来
public class TicketsVolatile implements Runnable{
private volatile int num = 100;
@Override
public void run() {
while (true)
{
if(num>0)
{
num--;
System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩"+num);
}
}
}
}
public static void main(String args[])
{
//volatile
TicketsVolatile ticketsVolidate = new TicketsVolatile();
Thread thread1 = new Thread(ticketsVolidate);
Thread thread2 = new Thread(ticketsVolidate);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread1.start();
thread2.start();
}
运行:
这里说明一下,每次运行的结果肯定不同。但是我要的结果还是得到了,就是没办法解决我们刚刚说的需要解决的问题。
这是为什么呢?下面是网上说的volatile的作用。
为什么还剩2张的时候,突然变成了15呢?
这里简单的说一下,为什么,volatile在多线程的时候不能满足要求。
注意我划线的地方,什么叫 可见性。
简单来说,就是 我们住在一起,冰箱里有10个苹果,我吃一个,你马上就知道了还剩9个。你再吃一个,我马上也能知道所剩的个数是9-1=8个。这里有同学肯定会问了,那不是应该是正确的吗,为什么会出现这种问题
因为 volatile只能满足可见性,但是没办法满足原子性(只针对在操作单一操作的情况可以保证原子性,只要是复合操作那么没办法保证(复合操作:i++,i=j+1等)(单一普通操作:i=i));
比如说num--;它会分三个步骤去执行:
问题就在于这三个操作,处理器可能不会一下就把他们执行完毕。有两种情况:1、可能说,A线程执行了1,2之后,B线程这时候获得了CPU的执行权。然后B线程执行完了之后,A线程通过程序计数器的帮助,将3这个步骤执行完。这时候就会出现这种情况:
但是请记住这句话:
1、窗口1卖出一张票,还剩98张 窗口2卖出一张票,还剩98张
这种情况为什么会出现呢?是因为,线程2将100-1=99,然后阻塞。接着线程1从主内存中获取值为99,然后99-1=98,(如上线语义定义所述)回写到主内存中。然后打印出来。此时主内存中的值就是98。然后线程2从主内存中读值98,然后打印。
2、出现显示异常的问题
这种情况出现的原因是:当线程1获得执行权之后,16-1=15;然后进入了打印方法中,
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
此时,已经进来了。但是还未执行synchronized这句话。CPU执行权就又被线程2夺走了,但是此时已经回写了主内存了。等到线程1再次获取cpu执行权时,由于打印语句的结构,它已经进入了。所以,他们不会再去主内存中取值了,因为在它让出cpu执行权之前,它已经读取过了。这个地方很绕,大家只需要记住,编译器执行的语句是通过编译之后的语句,不是像我们写的代码这样,一行可能对应编译器的多行。这就是为什么会出现这些问题的。如果理解不了的,大家请记住一点:volatile只适用于一写多读的情况,如果一个共享变量会被多个线程操作修改,那么一定不要使用它。
要记住的是:volatile变量写操作之后执行,也不一定会回写了主内存才会被切换。也可能在回写前,就被切换了!
这就是为什么,volatile有局限性,不建议使用在线程并发的操作中。
然后同步锁,最普遍使用的一个操作synchronized
先展示代码
public class TicketsRunnable implements Runnable{
private int count = 100;
private Object lock = new Object();
@Override
public void run() {
while (true)
{
//这里使用this也可以
synchronized (lock)
{
if(count>0)
{
count--;
System.out.println(Thread.currentThread().getName()+"卖了张票,还剩"+count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public static void main(String args[])
{
//一起卖一百张票 同步锁
TicketsRunnable ticketsRunnable = new TicketsRunnable();
Thread thread1 = new Thread(ticketsRunnable);
Thread thread2 = new Thread(ticketsRunnable);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread1.start();
thread2.start();
}
运行代码:
现在就可以了,这是因为
synchronized 既可以满足可见性,又可以满足有序性;
但是这里提一句,不用把不需要解决高并发的代码也扔到synchronized中,因为会大大影响性能。
OK,that's all;
接下来会把jvm原理的博客补齐,方便大家理解这模块的知识。
大家有什么问题可以提出来,我们一起探讨