您的位置:首页 > 编程语言 > Java开发

java 并发编程基础知识一

2017-01-04 17:14 816 查看
java并发这块在实际项目中也很少用到,因为什么图片加载,网络请求等框架都帮你封装好了,但是你如果想往更高的平台走,就必须要过这一关,也是进bat必面的知识点,特别是电商项目,当然了这只要是针对后台服务器人的开发要求,所以准备花点时间想系统的在这方面有所提高,所以得先把理论学好,理论学好了再结合好的三方框架,看别人怎么写的,然后自己去模仿写一个,这就是学习的主要套路,可能这博客一次性不能写完,但是大概这离放假还有20天会一直在学这方面知识,所以准备20天写完,大概写几篇这方面博客,为了到时候研究三方图片加载和网络加载框架做基础!

记得我刚做android外包去华为面试,第一个问题就是一个java程序main函数中开启了几个线程,当时就懵逼,心想就一个,但是面试官这样问,感觉肯定不止一个,所以就直接说不知道,结果就知道了我损失了几百万,这几年,怎么知道一个main方法中总共开启了几个线程么:

package cn.dangbei;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class ThreadNumTest {
/**
* @param args
*/
public static void main(String[] args) {
//获取java线程管理
ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
//不需要获取同步的monitor,synchronizer信息,仅获取线程和线程堆栈信息
ThreadInfo[] infos = threadMxBean.dumpAllThreads(false, false);
//遍历线程信息,打印出线程的名称和状态
for(ThreadInfo threadInfo:infos){
System.out.println(threadInfo.getThreadName()+"线程的状态的是"+threadInfo.getThreadState());
}
}
}

打印结果:

Attach Listener线程的状态的是RUNNABLE

Signal Dispatcher线程的状态的是RUNNABLE

Finalizer线程的状态的是WAITING

Reference Handler线程的状态的是WAITING

main线程的状态的是RUNNABLE

我们发现就简单的运行了上面的代码,可能会说就一个main线程,其实只是看到表面而已,解析下上面几个线程的意思:

Attach Listener:暂不明白

Finalizer:调用对象finalize方法的线程,用于垃圾回收

Reference Handler:清除Reference的线程

main:main线程,程序的入口

Signal Dispatcher:分发处理发送给JVM信号的线程

现在把创建线程的2种方式讲下:

第一种:通过继承Thread对象创建线程

package cn.dangbei;
public class ThreadDemo1 {
/**
* @param args
*/
public static void main(String[] args) {
TestThread tt = new TestThread();
tt.start();//开启线程
}
static class TestThread extends Thread{
@Override
public void run() {
super.run();
System.out.println("线程的名称为"+getName());
}
}
}

我们调用start()是让线程处于就绪状态,这个时候还没执行,等待cpu切换执行,Thread中的run()方法就是我们线程所要执行的任务代码,

第二种创建线程的方式实现Runnable接口

package cn.dangbei;
public class ThreadDemo2 {
public static void main(String[] args) {
TestThread1 tt = new TestThread1();
Thread thread = new Thread(tt);
thread.start();
}
static class TestThread1 implements Runnable{
public void run() {
System.out.println("线程的名称为"+Thread.currentThread().getName());
}
}
}

我们发现Thread类和Runnable接口中都有run方法,其实这是使用了静态代理,Thread类是代理的角色,

那么使用这二个有啥不同或者说优缺点呢?我们大部分都是用第二种方式创建线程的,因为它是实现了接口,如果你采用第一种的话,那么你就不能继承其他类了,因为java中继承是单继承的,这是一方面原因,还有就是如果对同一个数据进行多线程操作的话,只能采用这种方式,比如你去火车票买的票,如果使用第一种方式的话,你没new一个Thread的话,就相当于在堆内存中开辟了一个空间,票是独立一份的,每次new票每次都是重新给你赋值了,所以不行,

现在看一个有意思的代码:

new Thread(new Runnable() {
public void run() {
System.out.println("runnable---run--");
}
}){
@Override
public void run() {
super.run();
System.out.println("thread---run--");
}
}.start();

执行结果是只执行Thread类中自己的run()方法,如果没有复写Thread类中的run()方法就会执行Runnale中的run()方法

现在对Thread类几个方法做下解释

 isAlive() 判断线程是否活着(还在运行) 还是死了(thread中的run方法执行的逻辑执行完了或者有外界的中断),写个简单的程序测试:

package thread;
public class ThreadDemo1 {
public static void main(String[] args) {
Test test = new Test();
System.out.println("线程1---"+test.isAlive());
test.start();
System.out.println("线程2---"+test.isAlive());
try {
Thread.sleep(1000);//主线程休眠1s
System.out.println("线程3---"+test.isAlive());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Test extends Thread{
public void run() {
super.run();
System.out.println();
}
}

log:

线程1---false

线程2---true

线程3---false

这个很简单就不再做解释了

getName()和setName():前者是获取线程的名称,后者是给线程设置名字

package thread;
public class ThreadDemo1 {
public static void main(String[] args) {
Test test = new Test();
test.start();
System.out.println("获取线程的名称::"+test.getName());

Test test1 = new Test();
test1.setName("hello");
test1.start();
System.out.println("获取线程的名称::"+test1.getName());
}
}
class Test extends Thread{
public void run() {
super.run();
System.out.println();
}
}

log:

获取线程的名称::Thread-0

获取线程的名称::hello

这个说明如果不给线程设置名称,线程的名称就是Thread-(带数字),而给线程设置了名称,获取的时候就你设置的名称

activeCount():计算当前活着线程的个数

package thread;
public class ThreadDemo1 {
public static void main(String[] args) {
Test test = new Test();
test.start();

Test test1 = new Test();
test1.start();
System.out.println("获取线程的名称::"+test1.activeCount());
}
}
class Test extends Thread{
public void run() {
super.run();
System.out.println();
}
}

log:

有3个线程存活

这里求线程活着的个数包括主线程

stop()方法是终止线程的执行,但是这个方法在jdk中注解表示过时,不推荐使用,不推荐使用的原因是因为你一旦调用了stop()方法,它会释放所有的锁,如果你是在多线程环境下对共享变量的操作,这个时候就会导致数据的异常,

package thread;
public class ThreadDemo1 {
public static void main(String[] args) {
MultiThread t = new MultiThread();
Thread t1 = new Thread(t);
t1.start();
for (int i = 0; i < 5; i++) {
new Thread(t).start();
}
t1.stop();
}
}
class MultiThread implements Runnable {
int a = 0;
public void run() {
// 同步代码块
synchronized ("") {
a++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:"+ Thread.currentThread().getName() + ":a =" + a);
}
}
}


这个是6个线程,对a变量进行了操作,我们使用了线程同步,这样就防止出现a变量我们一想不到的情况发生,不调用t1线程的stop()方法运行如下:

线程:Thread-0:a =1

线程:Thread-5:a =2

线程:Thread-4:a =3

线程:Thread-3:a =4

线程:Thread-2:a =5

线程:Thread-1:a =6

调用t1线程的stop()方法:

线程:Thread-5:a =2

线程:Thread-4:a =3

线程:Thread-3:a =4

线程:Thread-2:a =5

线程:Thread-1:a =6

我们只所以在run()方法中sleep(100),是模拟stop()方法出现问题,如果不sleep()的话,t1线程调用stop()又可能5个线程都执行完了,就模拟不出stop()方法出现的异常情况,假如你这么终止线程,而且你是做金融项目,那你这辈子也就到头了,

线程中断jdk也给我们提供了方法

public void interrupt() //中断线程

 public static boolean interrupted()判断是否被中断

 public boolean isInterrupted()方法内部是调用private native boolean isInterrupted(boolean ClearInterrupted)这个方法:判断是否被中断,并清除当前中断状态:

package thread;

public class ThreadDemo1 {

public static void main(String[] args) {

         MultiThread t = new MultiThread();

          t.start();

          try {
Thread.currentThread().sleep(20);
t.interrupt();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

         

     }

  }
class MultiThread extends Thread {
 public void run() {
        while(true){
       
try{
       
if(!Thread.currentThread().isInterrupted()){//判断是否被中断
       
System.out.println("******************************************线程没有被中断***********************");
       
}
       
sleep(10);
       
}catch(Exception  e){
       
//抛出异常后会清除中断标志 所以下面的代码相当于重新设置中断标志
       
Thread.currentThread().interrupt();
       
}
        }
    }

}

你会发现其实你中断了,但是log一直在打印出来,说明线程没有被中断,但是如果线程调用了sleep()休眠了,这个时候你打断的话,会报中断异常,要在异常中处理中断,设置新的中断标志,才能有效的中断线程!

现在看下线程的挂起和重新执行:

public final void suspend()线程的挂起

public final void resume()线程的执行执行

上面2个方法应该是一起出现的,但是在jdk中都是声明已经过时的方法,过时的原因是因为suspend()这个函数不会释放锁,因为如果resume()发生在加锁之前容易导致死锁发生.、

join()方法在Thread中有三个重载的:

join()throws InterruptedException;  //无参数的join()等价于join(0),作用是一直等待该线程死亡
join(long millis, int nanos) throws InterruptedException;  //最多等待该线程死亡millis毫秒
join(long millis, int nanos) throws InterruptedException ; //最多等待该线程死亡millis毫秒加nanos纳秒


其实它内部是调用了wait()等待方法,

join()的意思就是等待这个线程死亡,如果join的线程不死亡,程序就会阻塞在那里,

package thread;
public class ThreadJoinTest {
public static void main(String[] args) throws InterruptedException {
JoinTest jsJoinTest = new JoinTest();
Thread t1 = new Thread(jsJoinTest);
Thread t2 = new Thread(jsJoinTest);
Thread t3 = new Thread(jsJoinTest);
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
System.out.println("主线程----");
}
}
class JoinTest implements Runnable{
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"---"+"i="+i);
}
}
}
log:

Thread-0---i=0

Thread-0---i=1

Thread-0---i=2

Thread-0---i=3

Thread-0---i=4

Thread-0---i=5

Thread-0---i=6

Thread-0---i=7

Thread-0---i=8

Thread-0---i=9

Thread-1---i=0

Thread-1---i=1

Thread-1---i=2

Thread-1---i=3

Thread-1---i=4

Thread-1---i=5

Thread-1---i=6

Thread-1---i=7

Thread-1---i=8

Thread-1---i=9

Thread-2---i=0

Thread-2---i=1

Thread-2---i=2

Thread-2---i=3

Thread-2---i=4

Thread-2---i=5

Thread-2---i=6

Thread-2---i=7

Thread-2---i=8

Thread-2---i=9

主线程----

这个多线程调用了join()方法是不是就达到了顺序执行的目的,这个面试的时候可能会被问到,线程调用了join()方法,就是等待这个线程执行完或者挂了,才会执行下一个线程,既然说join()内部是调用wait()方法,那么一定会有线程执行完后被通知,所以系统会在所有线程执行完后会调用notiyAll().

 public final void setDaemon(boolean on) 把线程设置为守护线程,但是必须在调用start()方法之前,调用setDaemon(true)方法

package thread;
public class ThreadDaemonTest {
public static void main(String[] args) {
DaemonTest dt = new DaemonTest();
dt.setDaemon(true);
dt.start();
}
}
class DaemonTest extends Thread{
@Override
public void run() {
super.run();
for(int i=0;i<100000;i++){
System.out.println("i="+i);
}
}
}
log:

i=0

i=1

i=2

i=3

i=4

i=5

i=6

i=7

i=8

i=9

i=10

i=11

循环了12次这个线程就完事了

守护线程一般都是在后台默默的做一些操作,就好比是android中的service,但是如果一个应用只有守护线程,那么jvm就会自然的退出!

线程还有优先级,默认是5,如果到Thread类的源码就有这个:

 /**

     * The minimum priority that a thread can have.

     */

    public final static int MIN_PRIORITY = 1;

   /**

     * The default priority that is assigned to a thread.

     */

    public final static int NORM_PRIORITY = 5;

    /**

     * The maximum priority that a thread can have.

     */

    public final static int MAX_PRIORITY = 10;

线程的优先级表示优先级高的线程可能会先执行,只是说可能性,并不代表一定会,比如在cpu切换时间片后再执行,如果看这个线程优先级比较高,可能会执行这个优先级高的线程,仅此而已!

package thread;
public class ThreadPrortyTest {
public static void main(String[] args) {
PrortyTest t1 = new PrortyTest();
PrortyTest t2 = new PrortyTest();
t1.setName("t1线程");
t2.setName("t2线程");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t2.start();
t1.start();
}
}
class PrortyTest extends Thread{
@Override
public void run() {
super.run();
for(int i=0;i<1000;i++){}
System.out.println(Thread.currentThread().getName()+"执行完了");
}
}
t2线程先执行,而且t2线程优先级高于t1,但是如果t1线程先启动,t2线程后执行,会发现,t1线程会先执行完,说明既然你设置了优先级也不代表cpu一定先执行你!就像中彩票一样,一个概率问题而已!

线程还有个sleep()方法,

java.lang.Thread sleep(long millis)方法被用来暂停当前线程的执行,暂停时间由方法参数指定,单位为毫秒。注意参数不能为负数,否则程序将会抛出IllegalArgumentException。

还有另外一个sleep(long millis, int nanos)方法,功能与上面方法相同,只不过暂停时间为millis毫秒数加上nanos纳秒数。纳秒允许的取值范围为0~999999(在源码中可以看到)

package thread;
public class ThreadSleepTest {
public static void main(String[] args) {
SleepTest st = new SleepTest();
st.start();
}
}
class SleepTest extends Thread{
@Override
public void run() {
super.run();
try {
sleep(-1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完毕");
}
}
打印结果:

Exception in thread "Thread-0" java.lang.IllegalArgumentException: timeout value is negative
at java.lang.Thread.sleep(Native Method)
at thread.SleepTest.run(ThreadSleepTest.java:13)

所以sleep()休眠的时间不能为负数.
package thread;
public class ThreadSleepTest {
public static void main(String[] args) {
SleepTest st = new SleepTest();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.setName("第一个");
t2.setName("第二个");
t1.start();
t2.start();
}
}
class SleepTest implements Runnable{
static int j=0;
public void run() {
synchronized (SleepTest.class) {//使用同一个锁
for(int i=0;i<10;i++){
j++;
System.out.println(Thread.currentThread().getName()+"线程执行"+"--"+"j="+j);
}
}
}
}
这个j最终的结果是20,如果我要线程休眠下也就是sleep()下:

package thread;
public class ThreadSleepTest {
public static void main(String[] args) {
SleepTest st = new SleepTest();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.setName("第一个");
t2.setName("第二个");
t1.start();
t2.start();
}
}
class SleepTest implements Runnable{
int j=0;
public void run() {
for(int i=0;i<10;i++){
j++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程执行"+"--"+"j="+j);
}
}
}




打印结果:

第二个线程执行--j=2第一个线程执行--j=2第二个线程执行--j=4第一个线程执行--j=4第二个线程执行--j=6第一个线程执行--j=6第一个线程执行--j=8第二个线程执行--j=8第一个线程执行--j=10第二个线程执行--j=10第一个线程执行--j=12第二个线程执行--j=12第二个线程执行--j=14第一个线程执行--j=14第一个线程执行--j=16第二个线程执行--j=16第一个线程执行--j=18第二个线程执行--j=18第二个线程执行--j=20第一个线程执行--j=20说明如果线程sleep()了,它会释放锁,cpu会让出时间去执行其他线程,但是如果在同步线程中使用了sleep()的话,就会等待这个线程sleep()指定的时间后才能让其他线程拿到锁才能执行

package thread;
public class ThreadSleepTest {
public static void main(String[] args) {
SleepTest st = new SleepTest();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.setName("第一个");
t2.setName("第二个");
t1.start();
t2.start();
}
}
class SleepTest implements Runnable{
int j=0;
public void run() {
synchronized (SleepTest.class) {
for(int i=0;i<10;i++){
j++;
System.out.println(Thread.currentThread().getName()+"线程执行"+"--"+"j="+j);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
结果我就不打印了,就是20最终的结果:

现在讲下线程的安全性:

所谓安全性一般都是对共享变量进行了修改后还能保证共享变量的正确性:

比较经典的例子就是买票的例子了,现在模拟这个

package thread;
public class TickerDemo {
public static void main(String[] args) {
Ticker ticker = new Ticker();
Thread t1 = new Thread(ticker);
Thread t2 = new Thread(ticker);
Thread t3 = new Thread(ticker);
Thread t4 = new Thread(ticker);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticker implements Runnable{
int ticker = 10;
public void run() {
while(true){
if(ticker<0){
break;//跳出循环
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ticker="+(ticker--));
}
}
}
这是4个线程去抢10张票,打印结果超过你想象:
ticker=10

ticker=8

ticker=9

ticker=7

ticker=6

ticker=6

ticker=6

ticker=5

ticker=3

ticker=4

ticker=4

ticker=2

ticker=1

ticker=0

ticker=-1

ticker=-2

这是为什么呢?画图解释



解决这个就要使用到一个关键字:synchronized,现在使用这个关键字对代码进行修改:

package thread;
public class TickerDemo {
public static void main(String[] args) {
Ticker ticker = new Ticker();
Thread t1 = new Thread(ticker);
Thread t2 = new Thread(ticker);
t1.setName("黄牛甲");
t2.setName("黄牛乙");
t1.start();
t2.start();
}
}
class Ticker implements Runnable{
private int ticker = 20;
public void run() {
while(true){
sale();
}
}
private synchronized void sale() {
if(ticker<=0){
return;//跳出循环
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"在售第"+(ticker--)+"张票");
}
}
运行结果:
黄牛甲在售第20张票
黄牛甲在售第19张票
黄牛甲在售第18张票
黄牛甲在售第17张票
黄牛甲在售第16张票
黄牛甲在售第15张票
黄牛甲在售第14张票
黄牛乙在售第13张票
黄牛乙在售第12张票
黄牛乙在售第11张票
黄牛乙在售第10张票
黄牛乙在售第9张票
黄牛乙在售第8张票
黄牛乙在售第7张票
黄牛乙在售第6张票
黄牛乙在售第5张票
黄牛乙在售第4张票
黄牛乙在售第3张票
黄牛乙在售第2张票
黄牛乙在售第1张票
发现是2个线程在抢票,并没有出现负数票,当然了要多运行几次,确保没问题!

线程安全的问题根本在于多个线程同时对共享变量操作,而解决多个线程同时对共享变量进行操作的就是要在线程上加上锁,当一个线程在访问共享变量时,另外线程不能对共享变量访问进行操作,

因为这时候另外线程不持有锁对象,上面的synchronized的就是这个原理,synchronized能使用再方法上,也能使用再同步代码块上,

Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

五、以上规则对其它对象锁同样适用

在这解释下阻塞:就是当一个线程访问一个共享变量,首先是判断这个锁是否释放了,如果锁没有被释放的话,就会阻塞在这里,等待上一个线程释放锁才能进的去,

好多书或者网上也叫临界区,是同一个意思!
注意:
1:锁对象值不能修改,如果锁定义的是一个string,如果你重新对这个string进行了赋值,那么这个锁就失效了,如果是我们自己定义的一个对象
,修改对象的属性值是没问题的,
2:并发访问要保证锁是同一个,不然无法起到同步的所用,而且容易导致死锁的发生!
现在演示几个常见使用锁错误的方式:
1:使用不同的锁不能保证变量的正确性
package cn.dangbei;
public class SynchronizeTest {
public static void main(String[] args) {
Thread	t1  = new Thread(new Ticker());
Thread	t2  = new Thread(new Ticker());
t1.setName("黄牛甲");
t2.setName("黄牛乙");
t1.start();
t2.start();
}
}
class Ticker implements Runnable{
int ticker = 10;
public void run() {
sale();
}
private synchronized void sale() {
while(true){
if(ticker<=0){
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买第"+(ticker--)+"票");
}
}
}
log:
黄牛乙买第10票
黄牛甲买第10票
黄牛甲买第9票
黄牛乙买第9票
黄牛甲买第8票
黄牛乙买第8票
黄牛乙买第7票
黄牛甲买第7票
黄牛甲买第6票
黄牛乙买第6票
黄牛甲买第5票
黄牛乙买第5票
黄牛甲买第4票
黄牛乙买第4票
黄牛甲买第3票
黄牛乙买第3票
黄牛甲买第2票
黄牛乙买第2票
黄牛乙买第1票
黄牛甲买第1票
你会发现你所买的票是不是搞错了,没错,这就是因为你Thread构造函数中传递的Runnable实例对象是你new出来的,所以当然是2个对象了
而不是同一个对象,故票也不是同一张票,这是使用锁不正确导致的


2:在线程访问中对锁进行了修改








                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: