说到线程,我们一定首先想到的是线程的创建,线程的创建一般有两种方式 一种是实现 Runnable 接口,另一种就是 继承 Thread 类 ,因为Java的单继承多实现机制,所以,优先选择 实现 Runnable 接口。
1 package test; 2 3 class ThreadTest extends Thread{ 4 5 public void run(){ 6 7 System.out.println("实现了Thread类"); 8 9 } 10 11 } 12 13 class RunnableTest implements Runnable{ 14 15 public void run(){ 16 17 System.out.println("实现了runnable接口"); 18 19 } 20 21 } 22 23 24 25 public class ThreadStartTest { 26 27 public static void main(String[] args) { 28 29 //直接继承了Thread ,创建一个Thread的实例 30 31 ThreadTest t1 = new ThreadTest(); 32 33 //t1启动 34 35 t1.start(); 36 37 //如果是实现了接口的线程类,需要用对象的实例作为Thread类构造方法的参数 38 39 Thread t2 = new Thread(new RunnableTest()); 40 41 t2.start(); 42 43 } 44 45 }
这儿就有一个我很久之前一直不了解的坑。那时因为不经常使用线程类,所以,对线程的开启仅停留在有两种方式上面。在使用继承的方式时,通过new xxxThread()的方式调用Start()方法,但使用接口的方式时 一直也是new xxxThread()d的方式,发现调不了start()方法,就调用了run()方法。.....其实这样是不对的,对于Java来说,通过new的方式调用内部run()方法一点问题都没有,但并不会开启新线程,那样做只会使用main线程。。正确的方式为Thread t2 = new Thread(new RunnableTest()); 然后调用start()方法。
总之一定要调用start()方法的。
1、那线程开启了就要考虑线程安全了
线程安全,说到底是数据的安全,我可不认识线程是谁,它安不安全,跟我没有半毛钱的关系。但数据不能不安全。这里就要提到内存了,因为,造成数据不安全的就是内存。
对于一个程序来说,就是一个进程,一个线程是其中的一部分。当系统为进程分配空间的时候,就会有公共空间(堆,公共方法区),和栈等。而造成不安全的就是这块公共的内存空间。
当一个线程在数据处理的过程中有另一个线程对数据进行了修改,就会造成数据不安全,程序混乱。这样我们就说这是线程不安全的。
1.1、怎么解决线程安全问题
解决线程安全问题,就要找到线程到底是怎么不安全的根本原因。其次安全与不安全是相对的。如果你的系统只有一个线程运行,或同一时间段不可能有两个线程同时运行。那也就不存在线程安全问题了。
那线程不安全是怎么造成的呢?
原因一:
“每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对该变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。”
原因二:线程抢夺
根本原因:线程内的关于外部变量的语境,与真实外部语境不一致。
针对这几个原因,我们来提出解决的方案。
解决方案一:避重就轻
对于原因一中 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,那就不要拷贝,我们所有的方法的参数都使用方法的局部变量,这样,就不会产生从主内存拷贝的问题。当每一个线程来执行方法的时候,系统都会为该线程分配一块属于自己的栈内存,这样,每个执行这个方法的线程都会有属于自己的局部变量,那么操作自己的局部变量就不会产生安全问题了。
解决方法二:只读不写
对于原因一种的对主内存的拷贝,有时候是不能不拷贝的那,我们就要看看能不能只允许它读取,不允许修改,也就是使用 final 修饰等...,这样,你只能看看我的数据,不能修改,就不会造成安全问题了。
解决方案三:人手一份
就是把变量分给每一个线程,让他们独立运行。在是实际的开发当中我们可能会遇到变量在线程中共享的需求。这时我们可以使用 ThreadLocal 定义线程的变量,使用ThreadLocal 定义的变量只在本线程中有效,这样也不会有安全问题。
1 @RestController 2 @RequestMapping("/test") 3 public class TestController { 4 8 9 static class MyThread implements Runnable { 10 Test test; 11 12 public MyThread(Test test) { 13 this.test = test; 14 } 15 16 @Override 17 public void run() { 18 for (int i = 0; i < 3; i++) { 19 test.dd(); 20 } 21 22 } 23 24 } 25 26 static class Test { 27 ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); 28 29 public void dd() { 30 try { 31 if (threadLocal.get() == null) 32 threadLocal.set(0); 33 Integer tt = threadLocal.get(); 34 tt += 60; 35 threadLocal.set(tt); 36 System.out.println(Thread.currentThread().getName() + "输出值为:" + threadLocal.get()); 37 38 Thread.sleep(100); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 } 43 } 44 45 public static void main(String[] args) { 46 Test test = new Test(); 47 for (int i = 0; i < 3; i++) { 48 MyThread myThread = new MyThread(test); 49 new Thread(myThread).start(); 50 51 } 52 } 53 }
在本例中有三个线程,变量使用 ThreadLocal 定义,通过 Test test = new Test(); 对线程传入相同的 Test 实例。这样避免使用不同的Test 实例 产生不同的ThreadLocal 变量对象。进而在 每个线程中循环3次进行 ThreadLocal 累加
上例运行结果如下:
可以看到线程之间没有产生累加,但同一线程中进行了累加。
解决方案四:加悲观锁
对于以上方法都不能满足我们的需求,那我们就只能采取更加严格的方式了,那就是加锁,只要加锁,那就要产生线程的阻塞,性能就会打折扣了。悲观锁就是悲观的认为只要我不加锁,那我的数据就会被其他线程修改,所以每次操作都要加锁,直到操作完成。
下面我将上面的代码修改一下,成为加悲观锁的情况:
1 public class TestController { 2 3 static class MyThread implements Runnable{ 4 Test test; 5 public MyThread(Test test){ 6 this.test=test; 7 } 8 @Override 9 public void run() { 10 for (int i = 0; i <3 ; i++) { 11 test.dd(); 12 } 13 14 } 15 16 } 17 static class Test{ 18 Integer threadLocal=0; 19 Lock lock=new ReentrantLock(); 20 public void dd(){ 21 try { 22 lock.lock(); 23 // Integer tt=threadLocal.get(); 24 threadLocal+=60; 25 // threadLocal.set(tt); 26 System.out.println(Thread.currentThread().getName()+ "输出值为:"+ threadLocal); 27 28 Thread.sleep(100); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 }finally { 32 lock.unlock(); 33 } 34 } 35 } 36 37 public static void main(String[] args) { 38 Test test=new Test(); 39 for (int i = 0; i <3 ; i++) { 40 MyThread myThread=new MyThread(test); 41 new Thread(myThread).start(); 42 43 } 44 } 45 }
在这个例子中我们把 ThreadLocal 替换为了普通的 Integer 变量,并使用了 Lock 进行加锁。我们同样开启三个线程,并在线程中进行3次循环。并执行累加,没有ThreadLocal 所有的线程公用一个变量,结果如下:
可以看到线程执行的顺序不一定,但输出的结果,没有出现错误。
解决方案五:加乐观锁
乐观锁与悲观锁相对,乐观锁认为,大概率没有线程会修改我的数据,如果修改了那就只能重新执行操作。如果在高并发情况下使用乐观锁,可能会更加浪费系统资源。那具体怎么操作呢?
- synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
- CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
volatile 关键字
我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用。