从本篇开始,我们将会逐渐总结关于 java 并发这一块的内容,也可以理解为是我的笔记,主要来自于一些博客和 java 书籍中的内容,所有的内容都是来自于他们之中并且加上了我自己的理解和认识。
我们将会从以下的几点理解 java 线程的一些概念:
- /*声明自己的一个线程类*/
- public class Test_thread extends Thread {
- //重写Thread类中的run方法
- public void run(){
- System.out.println("i am the thread");
- }
- }
- public class Test_Class {
- public static void main(String[] args){
- Test_thread thread = new Test_thread();
- thread.start();
- }
- }
- 输出结果:i am the thread
首先我们先了解一下,一个线程被创建之后,怎么才能启动运行,我们调用 thread.start(); 方法启动一个线程,首先就会执行我们重写的 run 方法(如果没有重写就会调用 Thread 类的 run 方法,什么也不做,这也是我们重写 run 方法的原因),也就是说 run 方法是一个线程的开始。有个疑问,为什么调用 start 方法,却执行了 run 方法了?其实调用 start 方法就是为线程的启动做准备操作,分配线程私有的堆栈资源,然后执行 run 方法。
下面我们看创建一个线程的第二种方式,实现接口 Runnable,并重写其中的 run 方法。
- public class Test_thread implements Runnable {
- public void run(){
- System.out.println("i am the thread");
- }
- }
- public class Test_Class {
- public static void main(String[] args){
- Test_thread thread = new Test_thread();
- thread.start();//编译错误
- }
- }
我们会发现虽然重写了 run 方法,但是在调用 start 方法的时候却编译错误,我们进入到 Runnable 接口的源代码中可以看到,只有一个抽象方法 run,所以没有启动线程的 start 方法,所以我们还是要借助 Tread 类。
- public class Test_Class {
- public static void main(String[] args){
- Thread thread = new Thread(new Test_thread());
- thread.start();
- }
- }
因为 Thread 类中有 start 方法,所以可以使用 Thread 的一个构造函数传入一个实现了 Runnable 接口的类型,构建一个 Thread 类。
三、线程的属性和状态
在一个多线程的程序中我们使用线程的一些属性来区别和辨认它们:
- private long tid;
- private volatile char name[];
- public static native Thread currentThread()
- private boolean daemon = false;
- private volatile int threadStatus = 0;
- public final static int NORM_PRIORITY = 5;
每个线程会有一个 tid 指定此线程的在所有线程中的排序,这个值是递增的,还有一个 name 指定该线程的名字,也可以使用 setName 设置一个优雅的线程名字,Thread 类还提供了一个方法返回当前正在占用 CPU 的线程对象。每个线程在某个时刻都是有状态的,属性 threadStatus 记录了当前对象线程的状态是什么,默认情况下,所有的线程的优先级都被置为 5,如果有特殊需要可以修改线程的优先级,使得某个线程可以优先得到运行。下面介绍线程的几种不同的状态。
- New:线程刚刚被创建,还没有被启动。
- Runnable:线程处于可运行的状态,但是不一定处于正在运行的状态(后面解释区别)
- Blocked:线程处于被阻塞的状态,一般是请求某资源不成功于是让出CPU阻塞自己(具体的区别后面说)
- Waiting:线程处于等待状态,一般是等待服务
- Terminated:线程终止,也就是线程被kill,释放其占用的资源
下面具体的说说不同状态下的线程的一些操作,首先看看 new,线程从此被创建,只是离运行状态还需要一些准备。Runnable 表示线程是可运行,至于是否已经处于运行状态还要看系统给的时间片是否用完,如果用完了就会将此线程放置在可运行线程队列的尾部,等待下次分配时间片,如果时间片没有用完,就是处于运行状态的(这也是为什么叫做 Runnable 而不是 Running 的原因)。接下来的两种状态 Blocked 和 waiting 都表示线程阻塞,需要让出 CPU。只是导致它们处于这种状态的原因不一样,具体的在我们介绍完 synchronized 关键字之后就会不言而喻了。
四、关键字 synchronized
先看一段代码:
- public class Test_thread extends Thread{
- public static int count = 0;
- public void run(){
- try {
- Thread.sleep((int) (Math.random() * 100));
- }catch(InterruptedException e){
- }
- count++;
- }
- }
- public class Test_Class {
- public static void main(String[] args){
- Test_thread[] thread = new Test_thread[1000];
- for(int a=0;a<1000;a++){
- thread[a] = new Test_thread();
- thread[a].start();
- }
- for(int a=0;a<1000;a++){
- try {
- thread[a].join();
- }catch (InterruptedException e){
- }
- }
- System.out.println(Test_thread.count);
- }
- }
按照直觉,创建 1000 个线程,每个线程随机睡觉并将公共变量增一,main 线程等待所有线程执行结束之后,输出公共变量的值。按照直觉答案应该是 1000,但是我们运行的结果每次都是不一样的,接近 1000 但每次都不一样。这是为什么呢?这其实就是简单的模拟了高并发,可能有几个线程睡了相同的时间,同时醒来获取的 count 值是相同的,这就导致这几个线程对 count 的操作被覆盖了。
- public class Test_thread extends Thread{
- public static int count = 0;
- public void run(){
- try {
- Thread.sleep((int) (Math.random() * 100));
- }catch(InterruptedException e){
- }
- /*使用关键字*/
- synchronized (Test_thread.class){
- count++;
- }
- }
- }
一个简单的关键字就可以轻松解决这样的高并发的问题。至于为什么这么写?在介绍完 synchronized 关键字之后,想必你就会知道了。
首先我们需要知道,每个对象都有一把内部锁。所以被 synchronized 关键字修饰的方法,其实是被加了内部对象锁。我们看代码:
- public class Counter{
- private int count;
- /*为实例方法加此关键字*/
- public synchronized int getCount(){
- return count;
- }
- }
为实例方法添加关键字,实际上就是给当前的对象加锁;括号中就是要加锁的对象。
- public class Counter{
- private int count;
- /*为实例方法加此关键字*/
- synchronized(this){
- return count;
- }
- }
对于静态方法,实际上就是为这个类加上锁;
- public class Counter{
- private static int count;
- public static synchronized int getCount(){
- return count;
- }
- }
- /*实际上给这个类加上锁*/
- public class Counter{
- private int count;
- synchronized(Counter.class){
- return count;
- }
- }
五、深入理解 synchronized 的一些特性
第一个性质是:可重入性。被 synchronized 修饰的方法中的所有操作都是原子操作,但是当我们需要在其中访问另外的一些需要锁的代码时候,可以直接获取别的锁。也就是当前的对象是可以获得多个锁的。
第二个性质是:内存的可见性。在我们的计算机中,其实有很多的操作并不是很 "干脆" 的,比如你向数据库中存数据时,其实很多时候一些待存入的数据积累在缓存中等到一定数据量的时候才会统一的存入数据库,但是在我们看来这些数据其实应该早就存入数据库了。这就导致了一个内存的不可见性问题。当然我们也是可以使用关键字 synchronized 来保证每次取出的数据都是最新的。在释放锁的时候会立即将数据协会内存,获得锁的时候会去读取最新的内容。
- public class Counter {
- private int count;
- public synchronized int getCount() {
- return count;
- }
- }
- /*每次获得该对象的锁之后,去获取最新的count数值*/
其实,如果仅仅是要保证内存的不可见性,使用 synchronized 关键字可能代价有点高,在这种情况下,我们可以使用关键字 volatile。
- public class Counter {
- /*使用关键字volatile*/
- private volatile int count;
- public int getCount() {
- return count;
- }
- }
- /*此关键字保证每次使用count的时候数据都是最新的*/
本篇文章到此结束,还是希望大家发现其中错误直接指出,方便我纠正错误,更新知识。java 并发系列文章,希望大家多多关注。
来源: http://www.cnblogs.com/yangming1996/p/6515830.html