JAVA多线程与高并发(一)[线程概念,同步synchronize关键字]

link-JAVA多线程与高并发系列[前言,大纲]

线程

程序,进程,线程,协成/纤程(fiber)

举个例子:
程序:QQ.exe是一个程序,存放在硬盘上,是一个静态的概念;
进程:当你双击它,QQ程序运行起来了,这就是一个进程.相对程序来说,进程是一个动态的概念;
线程:进程中最小的执行单元,一个进程内可以有多个线程同时执行命令

在JAVA中创建一个线程:
如果调用T1的run()方法,那就是简单的方法调用,代码还是在main线程中从上到下依次执行;
如果调用T1的start()方法,则会开启一个新的线程(记为thread1)执行,那么thread1和main线程同时执行,表现为控制台"T1"和"main"交替输出.

public class T01_WhatIsThread {
    private static class T1 extends Thread {
        @Override
        public void run() {
           for(int i=0; i<10; i++) {
               try {
                   TimeUnit.MICROSECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("T1");
           }
        }
    }

    public static void main(String[] args) {
        //new T1().run();
        new T1().start();
        for(int i=0; i<10; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main");
        }

    }
}

创建线程的3种方式?

1:Thread 2: Runnable 3:Executors.newCachedThrad

public class T02_HowToCreateThread {
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello MyThread!");
        }
    }

    static class MyRun implements Runnable {
        @Override
        public void run() {
            System.out.println("Hello MyRun!");
        }
    }

    public static void main(String[] args) {
        // 第一种,对象继承Thread,然后直接new该对象
        new MyThread().start();
        // 第二种,对象实现Runnable,该对象作为参数创建一个Thread对象
        new Thread(new MyRun()).start();
        // 第二种的变式,其实是匿名内部类的实例作为实现Runnable的对象,又加了lambda表达式简化代码
        new Thread(()->{
            System.out.println("Hello Lambda!");
        }).start();
        // 第三种线程池Executors.newCachedThrad等,其实最终实现也是前两种之一
    }

}

线程的常见操作

前言

CPU只管执行命令,对于CPU来说,没有线程的概念,它只是不断的从内存中去拿取指令去执行.
多线程呢,就是有好多个线程竞争着去给CPU发送命令,每个线程上的命令在CPU上执行一小会,多个线程的命令快速交替执行,这样看起来像是同时执行的.
各个线程和CPU之间,相当于有一个"等待队列",线程们在队列中排队,等着CPU从队列中随机找一个线程去执行;CPU执行某个线程的一小段命令后,不等执行完,就把它扔回等待队列,然后重新找一个线程执行,这样快速的切换感觉像是多个线程同时执行.如果线程执行完了,那就终结了自己的一生,不会再进入等待队列了.
线程操作:

  1. sleep(long millis):当前线程休息一定毫秒数,把执行命令的机会让给其他线程,睡够一定时间后进入等待队列,继续竞争CPU的执行机会
  2. yield():当前线程正在CPU上运行时,先退出一下,进去线程等待队列,然后大家再一起公平竞争CPU的执行机会.这个几乎用不到.
  3. join():假设俩个线程t1,t2,在t1中调用t2.join(),则此时执行t2的命令,t2执行完后t1继续执行,用于保证线程的执行顺序
  4. stop(): 在工程中尽量不要用,容易出现状态不一致问题,略复杂,就当没有这个方法吧
  5. interrupt():interrupt后会抛出一个异常,需要在上层catch改异常,然后做一些逻辑处理,控制程序流程.业务逻辑中也几乎没有必须interrupt的,也尽量不要用.
    假想的一个interrupt的场景:线程t1调用了sleep(两天)的方法,但是在一天后,需要让它醒来,那就调用t1.interrupt();前提是t1sleep时做好被interrupt的准备,即catchinterrupted异常,然后继续执行或者干点别的事.

线程的常见状态

线程状态迁移图:
线程在"等待队列"中等着被CPU执行时,就是Ready状态;线程正在被CPU执行时,是Running状态;Ready和Running合称为Runnable状态.
线程执行完后(或者被操作系统kill掉),进入terminated状态,结束了自己的一生,啥都不能干了(等待被GC回收).
JAVA多线程与高并发(一)[线程概念,同步synchronize关键字]_第1张图片获取线程状态的方法:new MyThead().getState();

同步

synchronize关键字(常见问题)

多个线程去访问同一个资源的时候,需要上锁,目的是保证状态的一致,就像数据库的事务一样.
类比场景:多个同学去厕所蹲坑.

锁的是什么?

是一个对象(包括class对象),拿到锁之后才能去执行某段代码.而不是锁的代码.

注意不要用String常量和Integer等基础数据类型作为锁的对象

不能用String的原因:所有的字符串常量都是同一个对象,假如引用的依赖包中用了String常量作为锁的对象,自己的程序也刚好用了相同String常量作为锁的对象,那么肯定会引起奇怪的问题.
也尽量不用String对象.
不能用Integer等基础数据类型的原因:Integer内部做了一些特殊处理,Integer的对象的值一旦改变,就会变成一个新对象

public class T {
	
	private int count = 10;
	private Object o = new Object();
	
	public void m() {
		synchronized(o) { //任何线程要执行线面的代码,必须先拿到对象o的锁
			count--;
			System.out.println(Thread.currentThread().getName() + " count = " + count);
		}
	}
	
}
synchronize底层怎么实现?什么是锁升级?synchronize一定比原子类慢?

JVM规范中没有任何要求,只要能保证功能完整就行.
HotSpot是这样的:对象头(64位)中,拿出2位来记录这个对象是不是被锁定了,mark word.

  1. JDK早期,synchronize是重量级的,每次都去找操作系统申请锁,效率很低;
  2. 后来改进了,引入了锁升级(可以看没错,我就是厕所所长一文)
    2.1. 偏向锁:当第一个线程去执行带synchronize方法时,先在对象头的markword上记录这个线程的线程号,不加锁;如果下次又是该线程执行那个方法,就直接访问不需获取锁.(偏向第一个线程)
    2.2. 自旋锁:如果有线程争用,则升级为自旋锁,比如线程t1正在访问带锁资源r,这时t2也要访问r,那么t2先不加锁,while(true)空执行一会,看t1是不是会马上释放锁.默认自旋10次,如果10后还得不到锁,则升级为重量级锁
    2.3. 重量级锁:去操作系统申请资源加锁
    Hotspot目前的实现,锁只能升级,不能降级.
    所以现在的synchronize并不一定比那些原子类慢,因为有锁升级
    自旋锁占用CPU,但是不去跟操作系统申请资源加锁,只是在用户态,不经过内核态.
    当加锁方法执行时间很长,或者线程数很多时,用操作系统锁比较好;
    当执行时间很短,且线程不太多时,用自旋锁合适.
synchronize的特点
  1. 可以在方法上加synchronize关键字,锁定当前对象(非静态方法),或者当前类的class对象(静态方法)
  2. synchronize(this)锁定当前对象;
  3. 对于非静态方法,synchronize(this)如果锁住了方法中的所有代码,那就和直接在方法上加synchronize是一样的;对于静态方法,方法上的synchronize相当于synchronize(T.class)
    (每一个.class文件,load到内存以后,会生成一个对应的Class对象)
  4. synchronize既保证可见性,又保证原子性
  5. 可重入.假如两个方法m1,m2都对同一个对象加了锁;如果m1中调用了m2,是可以再次获得锁的.(如果不允许重入,那就死锁了).可重入的概念是在同一个线程的基础上的.
class对象是单例的吗?

同一个ClassLoader内是单例的;多个ClassLoader间,不是单例;但是不同加载器之间不能互相访问.所以,可以认为是单例.

父类中有一个synchronize方法,在子类中调用,那么锁的是谁?

是子类对象,打印一下this即可证明:

public class T {
	synchronized void m() {
		System.out.println("super m start");
		System.out.println(this);
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("super m end");
	}
	
	public static void main(String[] args) {
		new TT().m();
		System.out.println("---");
		new TT().m2();
		System.out.println("---");
		new TT().m3();
	}
	
}

class TT extends T {
	@Override
	synchronized void m() {
		System.out.println("child m start");
		System.out.println(this);
		super.m();
		System.out.println("child m end");
	}

	synchronized void m2() {
		System.out.println("child m2 start");
		System.out.println(this);
		super.m();
		System.out.println("child m2 end");
	}

	void m3() {
		System.out.println("child m2 start");
		System.out.println(this);
		super.m();
		System.out.println("child m2 end");
	}
}
带锁的方法和不带锁的方法可以同时执行吗?

可以.

程序中如果抛出了异常,锁会被释放吗?

抛出异常会释放锁,所以一定要处理好异常,防止出现异常后被其他线程访问资源,从而导致各种状态不一致的问题.

锁优化

锁细化:锁住(synchronize包括)的代码,在能保证业务逻辑OK下,越少越好.
锁粗化:假如一段业务逻辑,中间有很多个细化的小锁,这些小锁的方法别的业务又不会调用,那就把这些小锁合并成一个大锁.

锁的属性(field)发生变化,会影响锁的使用效果吗?

锁的属性变化不会影响锁的功能;
但是如果锁的变量(或者说"引用")指向了别的对象,那就不是同一把锁了,会出问题.可以通过给变量加上final关键字避免这个问题.

你可能感兴趣的:(JAVA多线程与高并发)