Java多线程学习笔记

开始于2015410日:

Java的线程机制是抢占式的,这表示调度机制会周期性的中断线程,将上下文切换到另一个任务,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动他的任务。

如果当多个线程共同访问一个可变的状态变量的时候没事使用合适的同步,那么线程就会出现错误。有三种方式可以修复这个错误:

  1. 不在线程之间共享该状态变量。

  2. 将状态变量修改为不可改变的变量。

  3. 在访问状态变量的时候使用同步。

线程安全性的定义:当多个线程访问某个类的时候,这个类始终科比变现出来正确的行为,那么就说这个类是线程安全的。

无状态线程一定是安全的。访问过程之中没有共享的变量也没有对其他对象的引用,所以一定是安全的。

以关键字synchronized修饰的方法就是一个横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。

对于ThreadLocal的学习以及使用(详见ThreadLocalRef.html)。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMapThreadLocal类的一个静态内部类,它实现了键值对 的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。 ThreadLocal类通过操作每一个线程特有的ThreadLocalMap(这个Map与平时Util包下的Map不一样,要区分对待)副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自 己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。

对于native方法的理解:native关键字修饰的方法是Java调用了其他语言的方法,比如调用了C语言的相关方法,这里用native修饰的方法不能再出现abstract修饰符,因为被native方法修饰的方法已经有了实现体,而被abstract修饰的方法却没有实现体。往往在JDK源码之中会出现native修饰符(例如在Thread中大量出现了native),这是因为很多跟底层相关的东西都是用C语言来实现的,在JVM里面使用了部分C语言的方法来达到某些效果。而Java要想调用相关的方法,就可以直接用native关键字来修饰,这些方法往往在JVM中已经被写进去了,所以我们大可放心使用。

选择锁的原则:

同步是基于实际对象而不是对象引用的,多个变量可以引用同一个对象,变量也可以改变其值从而指向其他对象。因此,当选择一个对象锁时候,我们要根据实际对象而不是引用来考虑。作为一个原则,不要选择一个可能会在锁的作用域中改变的实例变量作为锁对象。

设置优先级:尽管JDK有多个优先级,但是他与多数操作系统都不能很好的映射。比如Windows上有7个优先级且不是固定的,所以这种映射关系也是不确定的。所以建议在使用优先级的时候只使用MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY三种级别。优先级为7的线程就会比优先级为6的线程有更高的机会去执行。当一个优先级比较低的线程很难获得CPU的执行时候,就称这种行为为CPU饥饿,所以程序员要保证每一个程序都不会产生CPU饥饿才行。详情见(Java线程P141)

在Java的线程中所有对象都自动含有单一的锁(也成为监视器)。当在对象上调用任意的synchronized方法的时候,此对象都被加锁。这时候该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁的时候才可以进行调用。例如一个Demo类中有如下方法:

           synchronized void f(){}

           synchronized void g(){}

对于以上方法,如果某个任务调用了f(),对于同一个对象而言,就只有等到f()调用结束之后并释放了锁之后其他任务才能调用f()个g().所以对于某个特定的对象来说,其所有的synchronized方法共享一个锁,这可以用来防止多个任务同时访问被编码为对象内存。

原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么他一定可以在可能发生的“上下文切换”之前执行完毕。

如果你声明一个域是violate的,那么只要对这个域产生了写操作,那么所有的操作都会看到这个修改。即使是用了本地缓存,情况也确实如此,violate域会议立即被写入到主存中,而读取操作就发生在主存中。如果多个任务在同时访问某个域,那么这个域就应该是violate的,否则,这个域就应该只能经同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法语句保护,那么久不必将其设置为violate

 线程的四种状态:

 新建(new):当线程被创建的时候,他只会短暂的处于这种状态。此时他已经分配了必须的系统资源,并执行了初始化。此时线程已经有资格获得CPU的资源了,之后调度器会将这个线程转变为可运行状态或者阻塞状态。

 就绪(Runnable):这种状态下,只要调度器把时间分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,他就可以运行。这不同于死亡和阻塞状态。

 阻塞(Blocked):线程能够运行,但是有个条件阻止他运行。当线程处于阻塞状态时,调度器会忽略该线程,不会分配给线程任何CPU时间,直到线程重新就如就绪状态,他才有可能执行操作。

 死亡(dead):处于死亡或者终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,他的任务已经结束,或者不再是可运行的。线程任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

  在使用wait()nodify()方法进行操作的时候,如果在同步块中使用这些东西,进行同步的锁必须和wait(),nodify()方法使用同一个锁。在wait()方法调用之前会释放相关的锁,方法返回的时候又重新获取到对象锁,也就意味着在wait()的时候可以有其他线程进来这个相关的代码块或者方法,从而会有更多的线程处于等待状态。当使用了nodify()以后这些等待的线程之中会有一种一个重新获得对象锁并且运行起来。当使用了nodeifyAll()方法以后这些等待的线程会全部唤醒,但是其中只有一个会重新获得对象锁继续往下执行。而sleep()则始终都持有对象锁。

  目前在JVM中最常用的线程调度方式:因为JVM中有11个优先级,所以暂且设置有14个链表存在。其中11个优先级分别对应一个链表(Runnable),一个链表用来存放所有初始状态的链表(new),一个链表用来存放所有阻塞状态的链表(Blocked),一个链表用来存放所有死掉的链表(dead),这样所有状态的线程在链表中都有相应的对应位置了。当有高优先级的线程获得调度的时候处于低优先级的链表都处于Runnable状态。假如说某一时刻切换到了低优先级的链表时候,链表情况如下:AB→null;在A执行了一点时间后线程又切到了高优先级的链表上去执行,然后高优先级的链表某一时刻进入阻塞状态,这时候线程又切回到了低优先级的链表上。此时因为这个链表是这样的:B→A→null,所以这个时候处于表头的B任务优先得到了执行,当CPU又将时间片切换到别的链表的时候A任务又来到了表头。(详见Java线程P145)

  

  

  Java调度线程的两种模型:

绿色线程模型:在这种模型中,线程是由虚拟机来调度的。他们大多数都遵循在上面提到的以优先级为基础的理想化调度。

本地线程模型:在这种模型之中,线程是由运行虚拟机的操作系统进行调度的。尽管他们都遵循我们 在此讨论的调度策略,但是因为操作系统不同,这种模型导致了Java线程调度之间的很多细微差别。

系统调用:要使用内核来完成某项工作调用,在Java中这包括对大多数流进行读/写操作,以及创建套接字。

关于守护线程:

典型的:垃圾回收线程就是一个守护线程。

Thread中有两个跟守护线程相关的方法, void setDeamon(Boolean on):将一个线程设置成为守护线程。Boolean isDeamon():判断是不是守护线程。注意:在调用setDeamon()之前线程一定不能运行,如果一个线程运行了,你就不能使一个用户线程转换成一个守护线程。

线程池:线程池中拥有有限数量的线程,但是其中的每一个线程都可以依次运行多个对象。当某个特定的虚拟机不能支持程序需要的线程个数时候,线程池是很有用的。这里是一个关于线程池参考的博客:http://www.oschina.net/question/565065_86540

避免死锁的最简单原则:一个同步方法永远不要调用另外一个同步方法。(尽管是一个被推荐的原则,但是并不理想).还有一种方式可以避免死锁:就是与我们要使用的对象相关联但是更高级的对象加锁。避免死锁的最有用原则是确保以同样的顺序获得锁。

线程的内容大致就这么多了,以后有机会再总结吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
嗨!很高兴回答你关于Java并发编程的问题。请问你想知道什么方面的内容呢?我可以分享一些学习笔记和建议给你。 1. 并发编程基础:了解并发编程的基本概念,如线程、进程、锁、同步等。学习Java中的并发编程模型以及相关的API,如Thread、Runnable、Lock、Condition等。 2. 线程安全性:学习如何保证多线程环境下的数据安全性,了解共享资源的问题以及如何使用同步机制来防止数据竞争和并发问题。 3. 线程间的通信:掌握线程间的通信方式,如使用wait/notify机制、Lock/Condition等来实现线程的协调与通信。 4. 并发容器:学习并发容器的使用,如ConcurrentHashMap、ConcurrentLinkedQueue等。了解它们的实现原理以及在多线程环境下的性能特点。 5. 并发工具类:熟悉Java提供的并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,它们可以帮助你更方便地实现线程间的协作。 6. 并发编程模式:学习一些常见的并发编程模式,如生产者-消费者模式、读者-写者模式、线程池模式等。了解这些模式的应用场景和实现方式。 7. 性能优化与调试:学习如何分析和调试多线程程序的性能问题,了解一些性能优化的技巧和工具,如使用线程池、减少锁竞争、避免死锁等。 这些只是一些基本的学习笔记和建议,Java并发编程是一个庞大而复杂的领域,需要不断的实践和深入学习才能掌握。希望对你有所帮助!如果你有更具体的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值