线程知识总结
实现线程有两种方法,一种是实现runnable接口,一种是继承Thread线程类。关于这两者的区别是前者只是实现了runnable接口的一个类而已并不是线程,后者继承Thread才是线程
我们来看看代码具体了解下
public class ThreadTest { public static void main(String[] args) { T1 t1=new T1("t1"); T2 t2=new T2("t2"); new Thread(t1).start();//1语句 从测试代码可以看t2才是一个真正意义上的线程对象而t1不是 t2.start();//2语句 这里值得注意的是语句1没执行完时 语句2就开始执行了语句1和语句2时并发执行的 } } class T1 implements Runnable { String name; public T1(String name) { this.name = name; } public void run() { for(int i=0;i<5;i++){ System.out.println("i am "+name); } } } class T2 extends Thread { String name; public T2(String name) { this.name = name; } public void run() { for(int i=0;i<5;i++){ System.out.println("i am "+name); } } }
有了线程我们可以并发处理,但这在一定程度上也会让出现读取脏数据,或者说丢失更新,数据混乱等情况的发生,例如单例模式中两线程同时访问公共单例获取方法 一线程准备给对象引用赋值时,一线程却读取对象的引用为空,也准备创建对象,并把引用赋予局部变量。
这样一来就造成单例对象数据混乱,出现两个实例,和我们的本意相违背。
为了同步,我们引入了synchronized关键字来同步。synchronized关键字既可以用于对象前,也可以用于方法修饰。synchronized(对象引用名) public synchronized void(){}
synchronized关键字保证资源的互斥性,即一段时间之类只能有一个线程访问。线程访问之前必须持有资源锁才能访问。
线程同步简单口述下,就不发代码了。下面我们来看下线程协作。
这里要用到关键字wait() notify() 方法 线程协作我们来看看经典代码生产者 消费者
public class Cooperation { public static void main(String[] args) { SuperMonitor monitor = new SuperMonitor(); new Producer(monitor,200); new Consumer(monitor,200); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } //生产 消费要平衡 生产后再消费,消费完后再生产 由管理员类控制 class SuperMonitor { boolean valueSet = false;//标志是否在生产 int count;//生产的物品总量 public SuperMonitor() { } public synchronized int get() { if(!valueSet) {//生产线程不再生产了,那么消费线程进入等待池,等待生产线程唤醒 try{ wait(); } catch (InterruptedException e) { e.printStackTrace(); } } valueSet = false; System.out.println("已消费"+count+"产品"); notify(); return count; } public synchronized void set(int i) { if(valueSet) {//如何生产线程可以生产了,生产线程进入等待池,等待消费线程唤醒 try{ wait(); } catch (InterruptedException e) { //e.printStackTrace(); } } valueSet = true; count=i; System.out.println("已生产"+count+"产品"); notify(); } } class Producer implements Runnable{ SuperMonitor monitor; int speed;//生产速度相关系数 public Producer(SuperMonitor monitor,int speed) { this.monitor = monitor; this.speed = speed; new Thread(this,"Producer").start(); } public void run() { int i=0; while(true){ monitor.set(++i); try { Thread.sleep((int)Math.random()*speed); } catch (InterruptedException e) { //e.printStackTrace(); } } } } class Consumer implements Runnable{ SuperMonitor monitor; int speed;//消费数量相关系数 public Consumer(SuperMonitor monitor,int speed) { this.monitor = monitor; this.speed = speed; new Thread(this,"Consumer").start(); } public void run() { while(true) { monitor.get(); try { Thread.sleep((int)Math.random()*speed); } catch (InterruptedException e) { e.printStackTrace(); } } } }
这里指的注意的是管理员类里get set 方法需要加锁因为多线程环境下可能找造成同时调用方法,否则会造成异常
以上是线程的一些基本功能。但是我们知道,线程同步增加加了线程竞争资源的等待时间,线程切换同样需要cpu开销,所以说进行线程优化是必不可少的。
线程性能优化大致有以下几点:
1 减少锁的持有时间
我们可以看下以下的代码
public synchronized void get() { h1(); get();//假设只有get()方法需要同步 h2(); }
代码这样写肯定是不妥的,因为h1();h2();方法并不需要同步,这样只会扩大了锁的持有时间,当线程持有锁后,必须执行完锁相关的任务才能释放锁。H1();h2();方法无疑是增加了任务的执行时间,也就锁的持有时间。所以说在需要加锁的方法前加上关键字才是恰当的做法。
2 减小锁粒度
减少锁粒度很经典的一个应用就是ConcurrentHashMap 当执行put方法时并会获得整个资源锁,而是通过HashCode()方法计算需要存放的段,然后获得段的锁。在多线程环境下,当有多个线程同时执行put操作时,由于hashcode()所得到不同的地址段(冲突少),所以说是对不同的段加锁。也就是说可以同时进行put操作,做到真正的并行。同理多个线程同时进行get操作时也不要获取整个ConcurrentHashMap的资源锁,而是获取某一个段的锁就可以了。
但是这种方式也存在一定的弊端,当要统计整个条目的数量时用锁方式,则要用循环的方式,一一加锁,再用循法的方法一一加数,最后再循法一一解锁。
3 粗化锁粒度
当有一连串对资源的锁申请时,会选择对锁进行粗化操作
public void get() { synchronized (lock){}; synchronized (lock){}; synchronized (lock){}; }
以上代码并不是合理的,因为频繁的加锁,释放锁需要很到的系统开销,将那一连串整合到一起,可以有效减少同步释放开销。
4 锁分离
我们都知道读操作和读操作时可以同时进行的,这也是锁分离的结果如果一个文件资源只有一个资源锁,那么一用户在读的同时其他用户是不能进行读操作的。就在这时我们可以引入都锁(非独占),写锁(独占)来是实现读读不互斥,读写互斥,写写互斥。
5 jvm 也提供了一系列支持来优化锁开销,自旋锁减少线程状态和上下文切换。
另有锁消除在运行时排除不可能存在锁竞争的资源的地方,节省锁请求资源时间。还有锁偏向,当一对象被一线程持有,若下次来之前此锁不被其他线程拥有,则再一次获取此锁时不会在同步。但锁偏向不适宜锁资源竞争激烈的环境因为此时有大量的线程切换,锁偏向基本不起作用,在此情况下禁用锁偏向反而有利于提升系统性能。