Java由浅入深之线程

52 阅读10分钟

基础概念

进程与线程

  • 进程是程序运行资源分配的最小单位
  • 线程是 CPU 调度的最小单位,必须依赖于进程而存在 CPU核心数与线程数关系
  • 多核心:指单芯片多处理器
  • 多线程: 简称 SMT.让同一个处理器上的多个线 程同步执行并共享处理器的执行资源
  • 核心数:线程:引入超线程技术前 是1:1,后是1:2 CPU时间片轮转机制
  • 每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间 。如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结来,则 CPU 当即进行切换。调度程序所要做的 就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾
  • 时间片的长度:时间片设得太短会导致过多的进程切换,降低了 CPU 效率: 而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常 是一个比较合理的折衷 并行与并发
  • 并行:指应用能够同时执行不同的任务
  • 并发:指应用能够交替执行不同的任务当谈论并发的时候一定要加个单位时间 高并发的意义与好处
  • 充分利用 CPU 的资源
  • 加快响应用户的时间
  • 可以使你的代码模块化,异步化,简单化 多线程程序需要注意事项
  • 线程之间的安全性
  • 线程之间的死锁
  • 线程太多了会将服务器资源耗尽形成死机当机:使用资源池,最好的示例是数据库连接 池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返 回池中。资源池也称为资源库

Java里的线程

Java程序天生就是多线程

  • 执行 main() 方法的是一个名称为 main 的线程
  • Java程序默认包含的线程:
  1. main //main 线程,用户程序入口
  2. Reference Handler//清除 Reference 的线程
  3. Finalizer // 调用对象 finalize 方法的线程
  4. Signal Dispatcher // 分发处理发送给 JVM 信号的线程
  5. Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等
  6. Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的 线程的启动
  • X extends Thread;,然后 X.start
  • X implements Runnable;然后交给 Thread 运行 线程的终止
  • 不安全中断: suspend()、resume()
和 stop() 在调用后,线程不会释放已经占有的资源
  • 安全中断: interrupt()中 断操作,通过 isInterrupted()判断是否被中断,静态方法 Thread.interrupted(),不过 Thread.interrupted() 会同时将中断标识位改写为 false
  • 阻塞状态:抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false
  • 不建议自定义一个取消标志位来中止线程的运行:run方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志
  • 处于死锁状态的线程无法被中断
  • 使用中断会更好
  1. 一般的阻塞方法,如 sleep 等本身就支持中断的检查
  2. 检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。 线程的常用方法与状态

线程的状态.png

  • start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现 的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常
  • 而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方 法并没有任何区别,可以重复执行,也可以被单独调用
  • join()方法:把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行 线程的优先级
  • 在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程 守护线程
  • 定义:Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作
  • 通过调用 Thread.setDaemon(true)将线程设置 为 Daemon 线程
  • 一般用不上,比如垃圾回收线程就是 Daemon 线程
  • 在 Java 虚拟机退出时 Daemon 线 程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中 的内容来确保执行关闭或清理资源的逻辑

线程间的共享协作

synchronized内置锁

  • 对象锁:对象锁是用于对象实例方法,或者一个对象实例上的
  • 类锁:类锁是用于类的静态 方法或者一个类的 class 对象上的(类锁只是一个概念上的东西,并不是真实存 在的,类锁其实锁的是每个类的对应的 class 对象)

对象锁的synchronized修饰方法与代码块

public class TestSynchronized1 {
    public void test1() {
        synchronized (this) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    public synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized1 myt2 = new TestSynchronized1();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                myt2.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
    }

}

运行结果

test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

上述的代码,第一个方法时用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也不可传入其他对象的实例;第二个方法是修饰方法的方式进行同步。因为第一个同步代码块传入的this,所以两个同步代码所需要获得的对象锁都是同一个对象锁,下面main方法时分别开启两个线程,分别调用test1和test2方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test1线程执行完毕,释放掉锁,test2线程才开始执行。(也有可能先执行完test2,?这是因为java编译器在编译成字节码的时候,会对代码进行一个重排序,也就是说,编译器会根据实际情况对代码进行一个合理的排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的)

类锁的修饰(静态)方法和代码块

public class TestSynchronized2 {
    public void test1() {
        synchronized (TestSynchronized.class) {
            int i = 5;
            while (i-- > 0) {
                System.out.println(Thread.currentThread().getName() + " : " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                }
            }
        }
    }

    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized2 myt2 = new TestSynchronized2();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
        //         TestRunnable tr=new TestRunnable();
        //         Thread test3=new Thread(tr);
        //         test3.start();
    }

}

运行结果

test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁

实例锁和类锁是不同的,两者可以并行

public class TestSynchronized {
    public synchronized void test1() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static synchronized void test2() {
        int i = 5;
        while (i-- > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException ie) {
            }
        }
    }

    public static void main(String[] args) {
        final TestSynchronized myt2 = new TestSynchronized();
        Thread test1 = new Thread(new Runnable() {
            public void run() {
                myt2.test1();
            }
        }, "test1");
        Thread test2 = new Thread(new Runnable() {
            public void run() {
                TestSynchronized.test2();
            }
        }, "test2");
        test1.start();
        test2.start();
        //         TestRunnable tr=new TestRunnable();
        //         Thread test3=new Thread(tr);
        //         test3.start();
    }

}

运行结果

test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test1 : 1
test2 : 1
test1 : 0
test2 : 0

上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的

既然有了synchronized修饰方法的同步方式,为什么还需要synchronized修饰同步代码块的方式呢

  1. synchronized的缺陷 当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。

  2. 当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面。例如在某个类的方法里面:这个类里面声明了一个对象实例,SynObject so=new SynObject();在某个方法里面调用了这个实例的方法so.testsy();但是调用这个方法需要进行同步,不能同时有多个线程同时执行调用这个方法。

  3. 这时如果直接用synchronized修饰调用了so.testsy();代码的方法,那么当某个线程进入了这个方法之后,这个对象其他同步方法都不能给其他线程访问了。假如这个方法需要执行的时间很长,那么其他线程会一直阻塞,影响到系统的性能。如果这时用synchronized来修饰代码块:synchronized(so){so.testsy();},那么这个方法加锁的对象是so这个对象,跟执行这行代码的对象没有关系,当一个线程执行这个方法时,这对其他同步方法时没有影响的,因为他们持有的锁都完全不一样

  4. 一种特例,就是上面演示的第一个例子,对象锁synchronized同时修饰方法和代码块,这时也可以体现到同步代码块的优越性,如果test1方法同步代码块后面有非常多没有同步的代码,而且有一个100000的循环,这导致test1方法会执行时间非常长,那么如果直接用synchronized修饰方法,那么在方法没执行完之前,其他线程是不可以访问test2方法的,但是如果用了同步代码块,那么当退出代码块时就已经释放了对象锁,当线程还在执行test1的那个100000的循环时,其他线程就已经可以访问test2方法了。这就让阻塞的机会或者线程更少。让系统的性能更优越