性能开销的最大问题:
1、内核态到用户态的的切换
2、上下文的保存(数据的保存)
在内核态与用户态的切换中同时伴随着 上下文的保存
线程是由进程创建的,是进程的一个实体,是具体干活的人,一个进程可能有多个线程。线程不独立分配内存,而是共享进程的内存资源,线程可以共享cpu的计算资源。
多线程创建的四种方式
1,实现Runnable接口
public class UseRunable implements Runnable{
@Override
public void run() {
System.out.println(2);
}
public static void main(String[] args) throws InterruptedException {
System.out.println(1);
new Thread(new UseRunable()).start();
new Thread().sleep(10);
System.out.println(3);
}
}
2,继承Thread类
public class UseThread extends Thread{
public void run(){
System.out.println(2);
}
public static void main(String[] args) throws InterruptedException {
System.out.println(1);
new Thread().sleep(12);
new UseThread().start();
System.out.println(3);
}
}
3,箭头函数
public static void main(String[] args) throws InterruptedException {
System.out.println(1);
new Thread(//new UserThread() {
// @Override
// public void run() {
// System.out.println(1);
// }
// }箭头函数
()-> System.out.println(2)
).start();
Thread.sleep(15);
System.out.println(3);
}
4,实现Callable接口传给FutureTask(可以有返回值)
开启多线程有利于做同一件事的时候可以采取多个分支的作用
提高运行效率
public class UseCallable implements Callable<Long> {
int from;
int to;
public UseCallable() {
}
public UseCallable(int from, int to) {
this.from = from;
this.to = to;
}
@Override
public Long call() throws Exception {
long res=0;
for (int i = from; i <to ; i++) {
res+=i;
}
return res;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//单线程创建的方式
long start = System.currentTimeMillis();
long tager = 0L;
for (int i = 0; i < 1000000000; i++) {
tager+=i;
}
long end = System.currentTimeMillis();
System.out.println(tager);
System.out.println(end-start);
//多线程的创建方式
start=System.currentTimeMillis();
long sum=0l;
FutureTask[] futureTasks=new FutureTask[50];
for (int i = 0; i < 50; i++) {
FutureTask futureTask=new FutureTask(new UseCallable(i*20000000,(i+1)*20000000));
new Thread(futureTask).start();
futureTasks[i]=futureTask;
}
for (int i = 0; i < futureTasks.length; i++) {
long res= (long) futureTasks[i].get();
sum+=res;
}
end=System.currentTimeMillis();
System.out.println(sum);
System.out.println(end-start);
}
}
守护线程的意义(随着主线程的存在而存在,主线程的消亡而消亡)
public class Deamon {
public static void main(String[] args) {
Thread thread1=new Thread(()->
{ int count=10;
Thread thread2=new Thread(()->{
while (true){
ThreadUtils.sleep(100);
System.out.println("我是守护线程");
}
});
/**
* 如果不设置为用户thread1的守护线程会进行一直打印
* 但是如果设置为thread1的守护线程会在thread1结束的时候毁
* 直接设置为thread1为守护线程会直接挂掉
*/
thread2.setDaemon(true);
thread2.start();
while (count>=0) {
ThreadUtils.sleep(300);
System.out.println("我是用户线程");
count--;
}
System.out.println("用户线程结束");
});
// thread1.setDaemon(true);
thread1.start();
}
}
线程的生命周期(创建->就绪->运行->终结)
不管等待还是阻塞都会重新返回就绪状态
多线程JMM存在的问题
- 1、指令重排(对编译的可以打乱)
一个对象的半初始化状态
Cow cow= new Cow();
new 创建
invokespecial init 初始化
ldc 入栈
可以先初始化再new
- 2、可见性(线程之间不可见的问题)
加入volatile可以强制转到主存区,而不是缓存区(避免了重排和可见性的问题)
-
List item as-if-serial语义 防止单线程结果被改变
-
happens-before语义 防止多线程结果被改变
读写操作的问题,线程之间不可见
在写入时volatile会变得线程可见从而使得线程之间互通
改写isOver 的值为ture跳出循环
public class visibility {
private *volatile* static boolean isOver = false;
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!isOver) {
}
System.out.println(number);
}
});
thread.start();
Thread.sleep(1000);
number = 50;
isOver = true;
}
}
3、线程争抢(线程与线程之间抢夺资源的问题)关于锁在下一篇博客中
增加方法同步锁synchronized
4、线程安全的实现方法
在并发量比较大的情况下还是用悲观锁解决
(1)数据不可变(final修饰)
在Java当中,一切不可变的对象(immutable)一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障的措施,比如final关键字修饰的基础数据类型,再比如说咱们的Java字符串。只要一个不可变的对象被正确的构建出来,那外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态,带来的安全性是最直接最纯粹的。比如使用final修饰的基础数据类型(引用数据类型不可以)、比如java字符串,而一旦被创建就永远不能改变。
(2)互斥同步(悲观锁并发策略)
互斥同步是常见的一种并发正确性的保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用,互斥是实现同步的一种手段,互斥是因、同步是果,互斥是方法,同步是目的。
在Java中最基本的互斥同步手段,就是synchronized字段,除了synchronize的之外,我们还可以使用ReentrantLock等工具类实现。
(3)非阻塞同步(乐观锁并发策略)
互斥同步面临的主要问题是,进行线程阻塞和唤醒带来的性能开销,因此这种同步也被称为阻塞同步,从解决问题的方式上来看互斥同步是一种【悲观的并发策略】,其总是认为,只要不去做正确的同步措施,那就肯定会出现问题,无论共享的数据是否真的出现,都会进行加锁。这将会导致用户态到内核态的转化、维护锁计数器和检查是否被阻塞的线程需要被唤醒等等开销。
随着硬件指令级的发展,我们已经有了另外的选择,基于【冲突检测的乐观并发策略】。通俗的说,就是不管有没有风险,先进行操作,如果没有其他线程征用共享数据,那就直接成功,如果共享数据确实被征用产生了冲突,那就再进行补偿策略,常见的补偿策略就是不断的重试,直到出现没有竞争的共享数据为止,这种乐观并发策略的实现,不再需要把线程阻塞挂起,因此同步操作也被称为非阻塞同步,这种措施的代码也常常被称之为【无锁编程】,也就是咱们说的自旋锁。我们用cas来实现这种非阻塞同步。
(4)无同步方案(线程各干各的变量到本地)
多个线程需要共享数据,但是这些数据又可以在单独的线程当中计算,得出结果,而不被其他的线程所影响,如果能保证这一点,我们就可以把共享数据的可见范围限制在一个线程之内,这样就无需同步,也能够保证个个线程之间不出现数据征用的问题,说人话就是数据拿过来,我用我的,你用你的,从而保证线程安全,比如说ThreadLocal。
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
public class Test {
private static Integer number=0;
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> threadLocal=new ThreadLocal<>();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(number);
for (int i = 0; i < 1000; i++) {
threadLocal.set(threadLocal.get()+1);
System.out.println("线程1打印出"+threadLocal.get());
}
}});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(number);
for (int i = 0; i < 1000; i++) {
threadLocal.set(threadLocal.get()+1);
System.out.println("线程2打印出" + threadLocal.get());
}
}});
thread1.start();
thread2.start();
}
}